2

我正在创建一个线程,然后说要等待它通过 WFSO 调用终止(下面是简化的伪代码,显然没有人想在创建线程后立即等待它)。

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

在主线程中

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);

我在做什么错/错过了什么?如果我没有 WFSO,线程将在大约 10 秒内完成。

编辑:使用 FreeOnTerminate=false 创建不会产生此问题。

4

1 回答 1

7

等待使用的TThread对象是一种竞争条件,因为一旦对象的方法退出,FreeOnTerminate=true可以随时释放该对象。因此,除非您可以保证您在线程事件被触发之前调用,否则您不能保证有一个有效的对象可以读取其属性。基本上,一旦线程的构造函数退出,使用时所有的赌注都将关闭,除非您使用事件处理程序。用于“创建和忘记”类型的线程。如果您出于任何原因需要引用线程,如果您不小心,使用会变得很危险。Execute()WaitForSingleObject() OnTerminateHandleFreeOnTerminate=trueOnTerminateFreeOnTerminate=trueFreeOnTerminate=true

如果您要等待一个TThread对象,那么在该对象上使用是没有意义的FreeOnTerminate=true,因为一旦等待完成,您就可以自己释放该对象。这样做可以确保对象在您手动释放它之前保持有效,因此您可以随时使用它Handle

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;

(您不需要Start()在构造函数内部调用,CreateSuspended=False而是使用,线程不会真正开始运行,直到构造函数退出后)

话虽如此,超时WaitForSingleObject()返回是不可能WAIT_TIMEOUT的。INFINITE但是它有可能返回WAIT_FAILED,例如当TThread对象在被调用之前被释放时WaitForSingleObject(),或者即使它仍在等待,从而在它被使用时Handle关闭。Handle

关于死锁,如果您为对象OnTerminate分配了一个事件处理程序TThread,则该处理程序将通过该TThread::Synchronize()方法调用,以便该处理程序在主线程中运行。但是,如果主线程在 上被阻塞WaitForSingleObject(),那么它就不能服务任何TThread::Synchronize()(或TThread::Queue())请求。因此,您最终会导致工作线程在主线程上等待而主线程在工作线程上等待 - 死锁。

为避免这种情况,您可以WaitForSingleObject()在一个短超时的循环中调用,这样您就可以CheckSynchronize()在等待时定期调用 RTL 的函数:

var h: THandle;

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

fst.Free;

使用阻塞等待时还有其他问题需要处理,例如SendMessage()从其他线程调用主线程。因此,您还需要为这些请求提供服务:

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;

或者,也将Classes.SyncEvent句柄添加到等待中(TThread::Synchronize()TThread::Queue()在有待处理请求时在内部发出信号):

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;

仅供参考,TThread有它自己的WaitFor()方法在线程上执行阻塞等待以终止,同时为TThread::Synchronize()/TThread::Queue()SendMessage()请求提供服务,类似于上面:

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

请注意,调用TThread::WaitFor()使用的TThread对象FreeOnTerminate=true也不安全。当内部循环迭代之间关闭时,它会失败并出现异常,EThread或者当它尝试访问已经被释放的对象时,它可能会直接崩溃。EOSErrorHandleHandleTThread

于 2020-04-04T00:45:01.573 回答