我有一些“FreeOnTerminate”工作线程,它们在开始执行时将其句柄添加到TThreadList上,在执行结束时将其从句柄中删除。他们还检查一个全局事件对象,该对象将通知他们取消工作。

以下是在主线程中运行的部分,该部分发出事件信号并等待可能的辅助线程结束。 WorkerHandleList是全局ThreadList

...

procedure WaitForWorkers;
var
  ThreadHandleList: TList;
begin
  ThreadHandleList := TWorkerThread.WorkerHandleList.LockList;
  TWorkerThread.WorkerHandleList.UnlockList;
  WaitForMultipleObjects(ThreadHandleList.Count,
      PWOHandleArray(ThreadHandleList.List), True, INFINITE);
end;

initialization
  TWorkerThread.RecallAllWorkers := TEvent.Create;
  TWorkerThread.WorkerHandleList := TThreadList.Create;

finalization
  TWorkerThread.RecallAllWorkers.SetEvent;
  WaitForWorkers;

  TWorkerThread.RecallAllWorkers.Free;
  TWorkerThread.WorkerHandleList.Free;

我认为这种设计的缺陷在于,我必须在等待线程的句柄之前就将列表解锁,因为这将导致死锁,因为线程本身将从同一列表中删除了它们的句柄。没有任何锁,上下文切换可能会导致线程释放自身,从而使WaitForMultipleObjects立即与WAIT_FAILED返回。我也不能使用另一个锁,因为WaitForMultipleObjects处于阻塞状态,并且我将无法从主线程释放该锁。

我可以通过多种方式修改此设计,包括不使用FreeOnTerminate线程,这将保证有效的句柄,直到它们被明确释放为止。或仅从主线程修改线程句柄列表。也许其他...

但是我想问的是,在不更改设计的情况下是否可以解决此类问题?例如,在将工作线程从列表中删除它们的句柄之前睡在工作线程代码中,或者调用SwitchToThread会导致所有非工作线程都运行吗?足够运行吗?

最佳答案

您对LockList()的使用是错误的,并且很危险。调用UnlockList()时,TList不再 protected ,并且随着工作线程从列表中将其自身删除而被修改。这可能在您有机会调用WaitForMultipleObjects()之前发生,或更糟糕的是在为其设置调用堆栈时。

您需要做的是锁定列表,将句柄复制到本地数组,解锁列表,然后在数组上等待。不要直接等待TList本身。

procedure WaitForWorkers;
var
  ThreadHandleList: TList;
  ThreadHandleArr: array of THandle;
begin
  ThreadHandleList := TWorkerThread.WorkerHandleList.LockList;
  try
    SetLength(ThreadHandleArr, ThreadHandleList.Count);
    for I := 0 to ThreadHandleList.Count-1 do
      ThreadHandleArr[i] := ThreadHandleList[i];
  finally
    TWorkerThread.WorkerHandleList.UnlockList;
  end;

  WaitForMultipleObjects(Length(ThreadHandleArr), PWOHandleArray(ThreadHandleArr), True, INFINITE);
end;

但是,即使有比赛条件。在实际输入WaitForMultipleObjects()之前,某些工作线程可能已经终止,因此破坏了它们的句柄。其余线程将在运行时破坏其句柄。无论哪种方式,它都会失败。在积极等待线程句柄的同时,您无法销毁它们。
FreeOnTerminate=True只能安全地用于您启动的线程,然后甚至忘记存在。当仍然出于任何原因仍需要访问来访问线程时,使用FreeOnTerminate=True是非常危险的(特别是因为这一警告,当TThread.WaitFor()FreeOnTerminate=True倾向于崩溃-线程句柄甚至TThread对象本身在被破坏时都会被破坏仍在使用!)。

您需要重新考虑您的等待策略。我可以想到一些替代方案:
  • 完全不使用WaitForMultipleObjects()。仅定期定期重新锁定列表并检查列表是否为空是更安全,但效率较低:
    procedure WaitForWorkers;
    var
      ThreadHandleList: TList;
    begin
      repeat
        ThreadHandleList := TWorkerThread.WorkerHandleList.LockList;
        try
          if ThreadHandleList.Count = 0 then Exit;
        finally
          TWorkerThread.WorkerHandleList.UnlockList;
        end;
        Sleep(500);
      until False;
    end;
    
  • 完全摆脱WorkerHandleList,并使用信号量或互锁计数器来跟踪已创建多少线程但尚未销毁它们。当信号量/计数器指示不存在更多线程时,退出等待。
  • 像Ken B建议的
  • 一样,继续使用WorkerHandleList,但是等待手动重置事件,该事件在将第一个线程添加到列表时重置(在线程构造函数中执行此操作,而不是在Execute()中),并在从中删除最后一个线程时发出信号列表(在线程析构函数中执行此操作,而不是在Execute()DoTerminate()中执行此操作)。
  • 10-04 23:37