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

Ser*_*yuz 9 delphi multithreading 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;
Run Code Online (Sandbox Code Playgroud)


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

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

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

Rem*_*eau 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;
Run Code Online (Sandbox Code Playgroud)

然而,即便是有竞争条件.一些工作线程可能已经终止,因此在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;
    
    Run Code Online (Sandbox Code Playgroud)
  2. WorkerHandleList完全摆脱并使用信号量或互锁计数器来跟踪已经创建了多少线程并且还没有被销毁.当信号量/计数器指示不再存在线程时退出等待.

  3. 像肯乙建议,继续使用WorkerHandleList但等待上获得第一线程被添加到列表中(这样做,在该线程的构造,而不是重置手动重置事件Execute()),并暗示当最后一个线程被从列表中移除(在线程析构函数中执行此操作,而不是在Execute()or DoTerminate())中执行.

  • 当列表被锁定时,您可以*复制*所有句柄.然后在列表解锁后,您可以等待这些重复项.即使原件关闭,它们仍然有效.当您选择关闭任何给定的副本时,存在增强机会. (3认同)

Ken*_*ssa 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;
Run Code Online (Sandbox Code Playgroud)

如果浪费任何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;
Run Code Online (Sandbox Code Playgroud)

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