Being kinda new to threading I'm running into an issue:
I've built a small wrapper for Synapse THTTPSend object to handle Async calls through means of a thread. All seems to go well until I exit the application and get this error (using madExcept exceptions handler) "System Error. Code: 1400. Invalid window handle."
main thread ($2d00):
0047f931 +091 x.exe System.SysUtils RaiseLastOSError
0047f88e +00e x.exe System.SysUtils RaiseLastOSError
006198c4 +064 x.exe Vcl.Controls TWinControl.DestroyWindowHandle
0061674c +0dc x.exe Vcl.Controls TWinControl.Destroy
0067487b +05b x.exe Vcl.ComCtrls TTabSheet.Destroy
00616781 +111 x.exe Vcl.Controls TWinControl.Destroy
00673218 +0b8 x.exe Vcl.ComCtrls TCustomTabControl.Destroy
0067529c +06c x.exe Vcl.ComCtrls TPageControl.Destroy
00616781 +111 x.exe Vcl.Controls TWinControl.Destroy
0073d95e +06e x.exe Vcl.Forms TScrollingWinControl.Destroy
0073f5d2 +1e2 x.exe Vcl.Forms TCustomForm.Destroy
0040b2d5 +015 x.exe System TObject.Free
005a034e +08e x.exe System.Classes TComponent.DestroyComponents
0073be06 +046 x.exe Vcl.Forms DoneApplication
00472520 +030 x.exe System.SysUtils DoExitProc
0040e0d9 +079 x.exe System @Halt0
I've tracked this down to accessing a listview, it goes like this:
If I skip that listview part the error never happens, so I think something may be wrong in my thread code that messes up with the vcl/gui, probably cause it's still running while the VCL is being accessed? If I check the listview there is something very odd with it after the thread ends, sometimes the listview isn't even visible, or the added items aren't clickable.
Listview part
procedure Tx.AddLog(url,DelURL: string);
begin
if Settings.OptEnableLogging.Checked then begin
With UploadsForm.ListView1.Items.Add do begin
Caption := DateTimeToStr(Now);
SubItems.Add(OriginalFilename);
SubItems.Add(url);
SubItems.Add('');
SubItems.Add(DelURL);
end;
SaveLoggingLog;
end;
With UploadsForm.ListView2.Items.Add do begin
Caption := DateTimeToStr(Now);
SubItems.Add(OriginalFilename);
SubItems.Add(url);
SubItems.Add('');
SubItems.Add(DelURL);
end;
end;
The thread object
type
TMySynHTTPAsync = class(TThread)
protected
procedure Execute; override;
private
sObj: TSynHTTP;
public
Constructor Create(SynObj: TSynHTTP);
Destructor Destroy; override;
end;
implementation
Constructor TMySynHTTPAsync.Create(SynObj: TSynHTTP);
begin
inherited Create(False);
Self.FreeOnTerminate := True;
sObj := SynObj;
end;
Destructor TMySynHTTPAsync.Destroy;
begin
//
inherited Destroy;
end;
Procedure TMySynHTTPAsync.Execute;
begin
With sObj do begin
try
case tCallType of
thPostString: ThreadResult := sObj.Post(tURL, tPostVars);
end;
except
//
end;
if Assigned(sObj.xOnAsyncRequestDone) then sObj.xOnAsyncRequestDone;
FThread := nil;
end;
end;
creating the thread
FThread: TThread;
procedure TSynHTTP.DoAsync;
begin
ThreadResult := False;
FThread := TMySynHTTPAsync.Create(Self);
FThread.Resume;
end;
I'm guessing this is the culprit, as it goes through all the GUI processing before the thread finishes.
if Assigned(sObj.xOnAsyncRequestDone) then sObj.xOnAsyncRequestDone;
How could I solve this?
You have posted a lot of code but not the key, relevant part. Specifically the implementation of your xOnAsyncRequestDone event handler/method (unless it literally only calls that log method that you posted).
This method is being executed in the context of the TMySynHTTPAsync thread and based on the behaviour you are describing - particularly the fact that Synchronize resolves your problem - it is highly likely that some activity in that event handler is creating a window handle.
That window handle is then owned by the HTTP Async thread, not the main application thread (sometimes referred to as the "VCL thread") that is otherwise running your application. When your application closes, the VCL thread performs some final housekeeping, destroying objects and windows etc. if one of those windows was created by some other thread this will cause problems.
Window handles are the strict property of the thread in which they were created. You cannot create a window handle in one thread and then destroy it in another.
NOTE: This is a fundamental of Windows, not Delphi.
Worth noting here is that window handles in VCL can often be created indirectly. You won't necessarily see an explicit creation of a control that marks the creation of the underlying window handle. It is quite common for window handles only to be actually created when required. Similarly changing the properties of a control can trigger the VCL to attempt to recreate the window for that control, destroying the current one in the process.
It should be fairly apparent that these mechanisms are highly vulnerable to problems that can arise when VCL methods are called by threads other than the VCL thread. This is why you will often here it said that "the VCL is not thread-safe".
The safest way to operate is to only manipulate VCL objects from code running in the VCL thread itself.
This is in fact precisely why Synchronize exists.
The mechanism that you are invoking by using Synchronize actually works to ensure that the method you are Synchronizing is execute on the VCL thread. If this is in fact creating a window handle then when the VCL thread later comes to destroy that window handle it is quite free to do so since it did in fact create it.
Hence your problem is solved.
The Synchronize mechanism is quite complex however, dealing (these days) with cross platform concerns among other things, and as a result may be overkill in this case.
If your code is specific to Windows, a possible alternate solution to this problem may be to exploit the fact that windows allows threads to send (or post) messages to windows in other threads. When those messages are received by those windows, they are then processed by that window's own thread just as all other messages to those windows are. i.e. you cannot end up interrupting a "click" message received by that window by suddenly jumping across to run the notification from the thread. That notification message simply has to wait it's turn while the window finishes processing that click message. For example.
You can think of this as a 'Synchronize' system "built-in" in to the OS.
So you could, for example, pass a window handle to a form (or control or anything with a window handle) to your HTTP async thread during initialisation, identifying a VCL window that wishes to receive the "request complete" or other notifications from the thread. The thread can then send notifications to that window handle using PostMessage or SendMessage which you could handle either by overriding the WindowProc on the form or using a declared message handler.
If the thread uses SendMessage() to send the notification, then it is automatically suspended and forced to wait until the message is received and processed by the window (in the VCL thread).
If the thread uses PostMessage() then the message is sent asynchronously and the thread can continue with other work without having to wait. The VCL thread will eventually pick up the message and process it.
This is not to say that I would recommend this alternative in this case. Although it does seem that it might be appropriate given that it does appear to be a simple "work is complete" notification in this case, without a more comprehensive understanding of your specific needs it is impossible to say which is most appropriate.
I mention it only to highlight the fact that alternatives do exist and that the key to safe, reliable threading is to understand the principles and the mechanisms involved.
The golden rule with threading is to not touch the GUI from another thread.
Depending on the situation this can be solved with Synchronize(), posting messages async (PostMessage()) or synchronized (SendMessage()). Another asynchronic option is using the TThread.Queue() call.
Last but not least, if you want to notify the GUI that the thread is done, assign an OnTerminate event handler to the thread. This event is executed in the main thread when the thread finishes executing. This is an example how it could be implemented:
type
TMySynHTTPAsync = class(TThread)
protected
procedure Execute; override;
private
sObj: TSynHTTP;
procedure MyTerminateHandler(Sender: TObject);
public
Constructor Create(SynObj: TSynHTTP);
Destructor Destroy; override;
end;
procedure TMySynHTTPAsync.MyTerminateHandler(Sender: TObject);
begin // Executed in the main thread
if Assigned(sObj) and Assigned(sObj.xOnRequestDone) then sObj.xOnRequestDone;
end;
procedure TMySynHTTPAsync.Execute;
begin
Self.OnTerminate := MyTerminateHandler; // Assign the OnTerminate event handler
...
end;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With