如何处理在阻止呼叫期间释放锁定的情况?

时间:2016-09-06 19:00:45

标签: multithreading delphi delphi-xe2

我有一些" 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会导致所有非工作线程都运行?够跑了吗?

2 个答案:

答案 0 :(得分:10)

您对LockList()的使用是错误的,也是危险的。一旦调用UnlockList()TList就不再受到保护,并且会在工作线程从列表中删除时自行修改。这可能发生在您有机会呼叫WaitForMultipleObjects()之前,或者更糟糕的是 WHILE 为其设置调用堆栈。

您需要做的是锁定列表,将句柄复制到本地阵列,解锁列表,然后等待阵列。不要直接等待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在{{1}时容易崩溃的警告 - 线程句柄甚至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并使用信号量或互锁计数器代替跟踪已创建且尚未销毁的线程数。当信号量/计数器指示不再存在线程时退出等待。

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

答案 1 :(得分:5)

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

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”过程中重置它。