OnFetchComplete асинхронного TADOQuery не синхронизирован с основным потоком

Когда используешь TADOQuery с [eoAsyncFetchNonBlocking] и прикрепление к OnFetchComplete событие, которое я обнаружил, что OnFetchComplete не выполняется в главном потоке (проверено в XE4 и XE8). Я предполагаю, что это ошибка *, так как большинство из нас будет работать в пользовательском интерфейсе над событиями такого типа. Я считаю, что это является источником некоторых проблем в более крупном проекте, и мне нужен обходной путь.

[РЕДАКТИРОВАТЬ] * После прочтения документации ADO я знаю, признаю, что это не может быть ошибкой, но проблема многопоточности остается.

Есть ли элегантный способ заставить код в этом обработчике выполняться в главном потоке? Я не хочу использовать таймер (но если это единственное решение, я его приму). В качестве альтернативы, есть ли здесь объект синхронизации ADO, которого я могу ждать, или какая-то другая форма сигнализации для поставщика ADO?

Вот упрощенный пример, который показывает, что проблема. Мой проект более сложен с фабрикой, создающей и заполняющей эти наборы данных, но это было бы здесь аналогично прикреплению набора данных к сетке внутри ADOQuery1FetchComplete,

unit Unit4;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.DB, Data.Win.ADODB, Vcl.StdCtrls;

type
  TForm4 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    ADOQuery1: TADOQuery;
    ADOConnection1: TADOConnection;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure ADOQuery1FetchComplete(DataSet: TCustomADODataSet;
      const Error: Error; var EventStatus: TEventStatus);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    FMainThreadID : DWORD;
  public
    { Public declarations }
  end;

var
  Form4: TForm4;

implementation

{$R *.dfm}

procedure TForm4.ADOQuery1FetchComplete(DataSet: TCustomADODataSet;
  const Error: Error; var EventStatus: TEventStatus);
begin
  Assert(FMainThreadID = GetCurrentThreadId); //this assertion fails!
  // I need UI code here to run  FMainThreadID
end;

procedure TForm4.Button1Click(Sender: TObject);
begin
   ADOQuery1.Open;
end;


procedure TForm4.FormCreate(Sender: TObject);
begin
    FMainThreadID := GetCurrentThreadId;
end;

end.

И DFM просто имеет запрос, установленный с ExecuteOptions = [eoAsyncFetchNonBlocking] а также OnFetchComplete обрабатываются.

object Form4: TForm4
  Left = 0
  Top = 0
  Caption = 'Form4'
  ClientHeight = 186
  ClientWidth = 258
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  OnCreate = FormCreate
  PixelsPerInch = 96
  TextHeight = 13
  object Button1: TButton
    Left = 24
    Top = 88
    Width = 75
    Height = 25
    Caption = 'Button1'
    TabOrder = 0
    OnClick = Button1Click
  end
  object ADOQuery1: TADOQuery
    Connection = ADOConnection1
    ExecuteOptions = [eoAsyncFetchNonBlocking]
    OnFetchComplete = ADOQuery1FetchComplete
    Parameters = <>
    SQL.Strings = (
      'SELECT * FROM TABLENAME')
    Left = 144
    Top = 16
  end
  object ADOConnection1: TADOConnection
    Connected = True
    ConnectionString = 
      'Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security In' +
      'fo=False;Initial Catalog=DBNAME;Data Source=.\INSTANCENAME'
    LoginPrompt = False
    Provider = 'SQLOLEDB.1'
    Left = 40
    Top = 16
  end
end

[ПРАВКА] Было предложено использовать TThread.Sychronize, но это не нить Delphi.

Если GetCurrentThreadId не является достаточным доказательством того, что обработчик вызывается из другого потока, здесь находятся стеки вызовов основного и проблемного потока (я добавил спящий режим в основной поток для хорошей меры)

Основной поток спит

:77d0c7bc ntdll.ZwDelayExecution + 0xc
:7745104f KERNELBASE.Sleep + 0xf
Unit6.TForm6.btnQueryClick($32BC00)
Vcl.Controls.TControl.Click
Vcl.StdCtrls.TCustomButton.Click
Vcl.StdCtrls.TCustomButton.CNCommand(???)
Vcl.Controls.TControl.WndProc((48401, 1344, 7275840, 0, 1344, 0, (), 1344, 111, (), 0, 0, ()))
Vcl.Controls.TWinControl.WndProc((48401, 1344, 7275840, 0, 1344, 0, (), 1344, 111, (), 0, 0, ()))
Vcl.StdCtrls.TButtonControl.WndProc((48401, 1344, 7275840, 0, 1344, 0, (), 1344, 111, (), 0, 0, ()))
Vcl.Controls.TControl.Perform(???,???,7275840)
Vcl.Controls.DoControlMsg(???,(no value))
Vcl.Controls.TWinControl.WMCommand((273, (), 1344, 0, (), 7275840, 0))
Vcl.Forms.TCustomForm.WMCommand((273, (), 1344, 0, (), 7275840, 0))
Vcl.Controls.TControl.WndProc((273, 1344, 7275840, 0, 1344, 0, (), 1344, 111, (), 0, 0, ()))
Vcl.Controls.TWinControl.WndProc((273, 1344, 7275840, 0, 1344, 0, (), 1344, 111, (), 0, 0, ()))
Vcl.Forms.TCustomForm.WndProc((273, 1344, 7275840, 0, 1344, 0, (), 1344, 111, (), 0, 0, ()))
Vcl.Controls.TWinControl.MainWndProc(???)
System.Classes.StdWndProc(2829362,273,1344,7275840)
:759b8e71 user32.CallNextHookEx + 0xb1
:759b90d1 ; C:\windows\SysWOW64\user32.dll
:759b932c ; C:\windows\SysWOW64\user32.dll
:759b9529 ; C:\windows\SysWOW64\user32.dll
:77d107d6 ntdll.KiUserCallbackDispatcher + 0x36
:759be4a9 ; C:\windows\SysWOW64\user32.dll
:711f19e4 ; C:\windows\WinSxS\x86_microsoft.windows.common-controls_6595b64144ccf1df_6.0.9600.17810_none_a9edf09f013934e0\comctl32.dll
:711f1a7b ; C:\windows\WinSxS\x86_microsoft.windows.common-controls_6595b64144ccf1df_6.0.9600.17810_none_a9edf09f013934e0\comctl32.dll
:759b8e71 user32.CallNextHookEx + 0xb1
:759b90d1 ; C:\windows\SysWOW64\user32.dll
:759bddd5 user32.CallWindowProcW + 0x95
Vcl.Controls.TWinControl.DefaultHandler(???)
:00532947 TWinControl.DefaultHandler + $EB
:00532836 TWinControl.WndProc + $5CA
:00544cdd TButtonControl.WndProc + $71
:004c9162 StdWndProc + $16
:759b8e71 user32.CallNextHookEx + 0xb1
:759b90d1 ; C:\windows\SysWOW64\user32.dll
:759ba66f ; C:\windows\SysWOW64\user32.dll
:759ba6e0 user32.DispatchMessageW + 0x10
:005bb158 TApplication.ProcessMessage + $F8
:00040000

Другой поток, вызывающий обработчик

Unit6.TForm6.QueryFetchComplete($288B3E0,nil,esOK)
Data.Win.ADODB.TCustomADODataSet.FetchComplete(nil,89849068,Pointer($3299D8) as _Recordset)
:6b7ab81d ; C:\Program Files (x86)\Common Files\System\ado\msado15.dll
:6b7ab4b6 ; C:\Program Files (x86)\Common Files\System\ado\msado15.dll
:6b7a17c8 ; C:\Program Files (x86)\Common Files\System\ado\msado15.dll
:6b7b616f ; C:\Program Files (x86)\Common Files\System\ado\msado15.dll
:69038991 ; C:\Program Files (x86)\Common Files\System\msadc\msadce.dll
:69038bd6 ; C:\Program Files (x86)\Common Files\System\msadc\msadce.dll
:69038d54 ; C:\Program Files (x86)\Common Files\System\msadc\msadce.dll
:69037a02 ; C:\Program Files (x86)\Common Files\System\msadc\msadce.dll
:69021205 ; C:\Program Files (x86)\Common Files\System\msadc\msadce.dll
:69038034 ; C:\Program Files (x86)\Common Files\System\msadc\msadce.dll
:77a07c04 KERNEL32.BaseThreadInitThunk + 0x24
:77d2ad1f ntdll.RtlInitializeExceptionChain + 0x8f
:77d2acea ntdll.RtlInitializeExceptionChain + 0x5a

1 ответ

Решение

По моему опыту, более простой способ заключается в использовании:

Синхронизировать или TThread.Queue

Это не ошибка или, по крайней мере, ошибка VCL. Это поведение обрабатывается провайдером, и мы не можем сказать, что оно не соответствует спецификации, потому что нет никакой спецификации о том, как управлять асинхронностью этих событий. Все, что говорится в спецификации, таково:

adAsyncFetchNonBlocking

Указывает, что основной поток никогда не блокируется при получении. Если запрошенная строка не была получена, текущая строка автоматически перемещается в конец файла.

Это пример кода, предупреждающего основной поток о завершении выполнения:

procedure TForm1.ADOQuery1FetchComplete(DataSet: TCustomADODataSet;
  const Error: Error; var EventStatus: TEventStatus);
begin
  TThread.Synchronize(nil,
    procedure
    begin
      ShowMessage('FetchData Completed');
    end
    );
end;

Обновить:

Я подтвердил это. Это будет работать для версий 6, 7, XE4 и XE7 (у меня нет другой версии здесь). Там нет ничего плохого в использовании Synchronize внедрить ваш код для выполнения в контексте основного потока. Кроме того, я хочу обратить ваше внимание на тот факт, что DataSet является просто указателем (фактически ссылкой) на ваш объект ADOQuery, поэтому вам не обязательно ссылаться на него в анонимном методе, это важный факт для более старых версий как 6 или 7, потому что анонимных методов не существует.

БОНУСНОЕ ЧТЕНИЕ: СОБЫТИЯ

Другие вопросы по тегам