Waiting for a TThread to finish with WaitForSingleObject()

2.4k Views Asked by At

I am creating a thread, and then say want to wait for it to terminate with a WFSO call (simplified pseudo-code below, obviously no one wants to wait on a thread right after creating it).

constructor TFileScannerThread.Create(Parameters)
begin
/// assign parameters to private variables, and call
inherited Create(true);  //Create Suspended
FreeOnTerminate := true; 
Start; 
end;

In the main thread

fst := TFileScannerThread.Create(BaseFolder, ScanMode, AbortEvent);

///  I presume, the next call would block the main thread until the child thread is done
///  but, it seems to create a deadlock & WFSO never returns
///  I get a WAIT_TIMEOUT if I use a finite wait time
WaitForsingleObject(fst.Handle, INFINITE);

What am I doing wrong/missing? If I do not have the WFSO the thread completes in about 10 seconds.

Edit: Creating with FreeOnTerminate=false does not create this issue.

1

There are 1 best solutions below

3
On

Waiting on a TThread object that uses FreeOnTerminate=true is a race condition, as the object could be freed at any moment once its Execute() method has exited. So unless you can guarantee that you are calling WaitForSingleObject() before the thread's OnTerminate event is fired, then you are not guaranteed to have a valid object on which to read its Handle property. Basically, once the thread's constructor exits, all bets are off when using FreeOnTerminate=true, unless you use an OnTerminate event handler. FreeOnTerminate=true is meant for "create and forget" kind of threads. If you need to refer to a thread for any reason, using FreeOnTerminate=true becomes dangerous if you are not very careful.

If you are going to wait on a TThread object, there is no point on using FreeOnTerminate=true on that object, since you can just free the object yourself once the wait is complete. Doing so ensures the object remains valid until you manually free it, thus you can use its Handle at any time:

constructor TFileScannerThread.Create(Parameters)
begin
  inherited Create(False);
  FreeOnTerminate := false;
  // assign parameters to private variables
end;
fst := TFileScannerThread.Create(BaseFolder, ScanMode, AbortEvent);
...
WaitForSingleObject(fst.Handle, INFINITE);
fst.Free;

(you don't need to call Start() inside the constructor, use CreateSuspended=False instead, the thread will not actually begin running until after the constructor exits)

That being said, it is impossible for WaitForSingleObject() to return WAIT_TIMEOUT on an INFINITE timeout. But it is possible for it to return WAIT_FAILED, such as when the TThread object is freed before WaitForSingleObject() is called, or even while it is still waiting on the Handle, thus closing the Handle while it is being used.

With regard to a deadlock, if you have an OnTerminate event handler assigned to the TThread object, that handler gets called via the TThread::Synchronize() method so that the handler runs in the main thread. But, if the main thread is blocked on WaitForSingleObject(), then it can't service any TThread::Synchronize() (or TThread::Queue()) requests. Thus, you end up with the worker thread waiting on the main thread while the main thread is waiting on the worker thread - deadlock.

To avoid that, you can call WaitForSingleObject() in a loop with a short timeout so that you can call the RTL's CheckSynchronize() function periodically while you are waiting:

var h: THandle;

h := fst.Handle;
while WaitForSingleObject(h, 500) = WAIT_TIMEOUT do
  CheckSynchronize;

fst.Free;

There are other issues to deal with when using a blocking wait, like SendMessage() calls from other threads to the main thread. So you would need to service those requests as well:

var
  h: THandle;
  ret: DWORD;
  msg: TMsg;

h := fst.Handle;
repeat
  case MsgWaitForMultipleObjects(1, h, False, 1000, QS_SENDMESSAGE) of
    WAIT_OBJECT_0, WAIT_FAILED: Break;
    WAIT_OBJECT_0 + 1: PeekMessage(msg, 0, 0, 0, PM_NOREMOVE);
    WAIT_TIMEOUT: CheckSynchronize;
  end;
until False;

fst.Free;

Alternatively, add the Classes.SyncEvent handle to the wait as well (TThread::Synchronize() and TThread::Queue() signal it internally when there are requests pending):

var
  h: array[0..1] of THandle;
  ret: DWORD;
  msg: TMsg;

h[0] := fst.Handle;
h[1] := SyncEvent;
repeat
  case MsgWaitForMultipleObjects(2, h, False, INFINITE, QS_SENDMESSAGE) of
    WAIT_OBJECT_0, WAIT_FAILED: Break;
    WAIT_OBJECT_0 + 1: CheckSynchronize;
    WAIT_OBJECT_0 + 2: PeekMessage(msg, 0, 0, 0, PM_NOREMOVE);
  end;
until False;

fst.Free;

FYI, TThread has its own WaitFor() method that performs a blocking wait on the thread to terminate, while servicing TThread::Synchronize()/TThread::Queue() and SendMessage() requests, similar to above:

fst := TFileScannerThread.Create(BaseFolder, ScanMode, AbortEvent);
...
fst.WaitFor;
fst.Free;

Just note that calling TThread::WaitFor() on a TThread object that uses FreeOnTerminate=true is not safe, either. It will either fail with an EThread or EOSError exception when the Handle is closed between internal loop iterations, or it will likely just crash outright when it tries to access the Handle of a TThread object that has already been freed.