如何处理阻塞调用中需要释放锁的情况?

How to handle a situation that requires to release a lock during a blocking call?

我有一些“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 立即变为 return WAIT_FAILED。我也不能使用另一个锁,因为 WaitForMultipleObjects 正在阻塞,我无法从主线程释放锁。

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

但是我想问的是,有没有不改变设计就可以解决这类问题的方法呢?例如,在工作线程代码从列表中删除句柄之前是否会在工作线程代码中休眠,或者调用 SwitchToThread 会导致所有非工作线程都有 运行?够了运行?

假设一些事情(只有主线程会启动辅助线程等),解决问题的最简单方法是这样的:

procedure WaitForWorkers;
var
  ThreadHandleList: TList;
  iItemCount : Integer;
begin
  repeat
    ThreadHandleList := TWorkerThread.WorkerHandleList.LockList;
    try
      iItemCount := ThreadHandleList.Count 
    finally
      TWorkerThread.WorkerHandleList.UnlockList;
    end;
    if iItemCount = 0 then
      BREAK;
    sleep(Whatever_is_suitable);
  until False;
end;

如果不能接受浪费任何 cpu 周期,或者等待时间超过必要的时间,您可以创建一个事件并等待它,并让所有线程通过相同的函数将它们自己从列表中删除。

procedure WaitForWorkers;
begin
  Event.WaitFor(INFINITE);
end;

procedure RemoveHandleFromList(AHandle : THandle);
var
  ThreadHandleList: TList; 
  idx : Integer;
begin
  ThreadHandleList := TWorkerThread.WorkerHandleList.LockList;
  try
    idx := ThreadHandleList.IndexOf(Pointer(AHandle));
    if idx >= 0 then
    begin
      ThreadHandleList.Delete(idx);
      if ThreadHandleList.Count = 0 then
        Event.SetEvent;
    end; 
  finally
    TWorkerThread.WorkerHandleList.UnlockList;
  end;
end;

在这种情况下,您可能希望使用手动重置事件并在 "AddHandleToList" 过程中重置它。

您对 LockList() 的使用是错误的,而且很危险。只要您调用 UnlockList()TList 就不再受保护,并且会随着工作线程从列表中删除它们自己而被修改。这可能发生在您有机会调用 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() 之前,一些工作线程可能已经终止,因此销毁了它们的句柄。其余线程将销毁它们的句柄 WHILE 它是 运行。无论哪种方式,它都失败了。您不能在积极等待线程句柄时销毁它们。

FreeOnTerminate=True 只能安全地用于您启动的线程,然后 甚至忘记存在 。当您出于 任何原因 仍然需要访问线程时使用 FreeOnTerminate=True 是非常危险的(特别是因为这个警告 TThread.WaitFor() 往往会崩溃FreeOnTerminate=True - 线程句柄甚至 TThread 对象本身在仍在使用时被销毁!)。

您需要重新考虑您的等待策略。我可以想到一些替代方案:

  1. 根本不要使用 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;
    
  2. 完全摆脱 WorkerHandleList 并使用信号量或互锁计数器来跟踪已创建和尚未销毁的线程数。当semaphore/counter表示没有更多线程存在时退出等待。

  3. 就像 Ken B 所建议的那样,继续使用 WorkerHandleList 但等待手动重置事件,该事件在第一个线程添加到列表时被重置(在线程构造函数中执行此操作,不在 Execute()) 中,并在最后一个线程从列表中删除时发出信号(在线程析构函数中执行此操作,而不是在 Execute()DoTerminate() 中)。