2015-06-17 11 views
7

Usando TADOQuery con [eoAsyncFetchNonBlocking] e fissaggio a OnFetchComplete evento ho trovato che OnFetchComplete non viene eseguito nel thread principale (testato in XE4 e XE8). Presumo che questo sia un bug *, dato che molti di noi lavoreranno nell'interfaccia utente per questo tipo di evento. Credo che questo sia la fonte di alcuni problemi in un progetto più ampio e ho bisogno di una soluzione alternativa.Asynchronous TADOQuery non synchonized a thread principale

[EDIT] * Dopo aver letto la documentazione di ADO, so che questo potrebbe non essere un bug, ma il problema del multithreading rimane.

C'è un modo elegante per forzare l'esecuzione del codice in questo gestore sul thread principale? Non voglio usare un timer (ma se è l'unica soluzione che prenderò). In alternativa, c'è un oggetto di sincronizzazione ADO che posso aspettare qui o qualche altra forma di segnalazione al provider ADO?

Ecco un esempio semplificato che mostra che il problema. Il mio progetto è più complesso con una fabbrica che crea e riempie questi set di dati, ma sarebbe analogo qui collegare il set di dati a una griglia all'interno di 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. 

E il DFM deve semplicemente la query set con ExecuteOptions = [eoAsyncFetchNonBlocking] e OnFetchComplete maneggiato.

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 

[EDIT] Un suggerimento è stato fatto da usare TThread.Sychronize, ma questo non è un filo Delphi.

Se il GetCurrentThreadId non è una prova sufficiente che il gestore si chiama da un altro thread qui sono gli stack di chiamate del thread principale e problematico (ho aggiunto un sonno nel thread principale per buona misura)

thread principale sonno

: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 

altro thread chiamante il gestore

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 
+0

'GetCurrentThreadId' identifica il thread chiamante. Se gli ID non corrispondono, deve essere un thread separato. –

+1

È possibile utilizzare l'handle di finestra del modulo (o assegnarne uno) a 'PostMessage' con il proprio messaggio utente nel gestore di eventi' OnFetchComplete', che di fatto viene eseguito al di fuori del contesto del thread principale. – kobik

+1

Questo evento si svolge effettivamente in un'altra discussione, come ha detto Kobik, la tua migliore opzione è usare 'PostMessage', puoi trovare un esempio [qui] (http://stackoverflow.com/a/26058386/800214). – whosrdaddy

risposta

3

Nella mia esperienza il modo più semplice è quello di utilizzare:

Synchronize o TThread.Queue

Questo non è un bug o almeno non un bug VCL. Questo comportamento è gestito dal provider e non possiamo dire che non stia seguendo lo specification perché non ci sono specifiche su come gestire l'asincronia di quegli eventi. Tutte le specifiche dice è la seguente:

adAsyncFetchNonBlocking

Indica che il thread principale non blocchi durante il recupero. Se la riga richiesta non è stata recuperata, la riga corrente si sposta automaticamente alla fine del file.

Questo è un esempio di codice avvertimento thread principale che l'esecuzione è completata:

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

Aggiornamento:

ho confermato questo. Funzionerà per le versioni 6, 7, XE4 e XE7 (non ho altra versione qui). Non c'è niente di sbagliato nell'usare Synchronize per iniettare il codice per l'esecuzione nel contesto del thread principale. Inoltre, voglio attirare la tua attenzione sul fatto che DataSet è semplicemente un puntatore (in realtà un riferimento) al tuo oggetto ADOQuery, quindi non devi necessariamente fare riferimento al tuo metodo anonimo, questo è un fatto importante per le versioni precedenti come 6 o 7, perché i metodi anonimi non esistono.

BONUS READING: EVENTS

+0

'OnFetchProgress' e' OnFetchComplete' sono gli unici due eventi che mostrano questo comportamento. Il problema è che non so come sincronizzare in questo caso. Questo almeno non sembra originario come un Delphi TThread, ma come originato dal provider OLEDB. Se fosse un Delphi TThread potrei usare TThread.Synchronize, ma qui non ho controllo di flusso. –

+0

Il thread non è originato da Delphi, come ho detto. Non c'è niente di sbagliato nell'usare Synchronize o TThread.Queue perché il thread del provider viene creato all'interno dello spazio dell'indirizzo del processo, si avrà comunque accesso ai controlli Main Form – EProgrammerNotFound

+0

Che tipo di controllo di flusso aggiuntivo è necessario? – EProgrammerNotFound

Problemi correlati