如何在关闭应用程序之前等待所有匿名线程都终止?

lok*_*oki 5 delphi

我遇到一个尴尬的问题。在我的应用程序中我经常这样做

TThread.createAnonymousThread(
  procedure
    ....
  end).start
Run Code Online (Sandbox Code Playgroud)

我遇到的问题是,当我关闭应用程序的主窗体时,有时在 Tform.destroy 完成后,其中一些 AnonymousThread 仍然存在。他们是我的 Tform.destroy 中等待所有这些 AnonymousThread (在整个应用程序中各处创建的)成功终止后再继续的方法吗?

我发现这种方法可以列出所有正在运行的线程(来自How can I get a list with all the thread Created by my application):

program ListthreadsofProcess;

{$APPTYPE CONSOLE}

uses
  PsAPI,
  TlHelp32,
  Windows,
  SysUtils;

function GetTthreadsList(PID:Cardinal): Boolean;
var
  SnapProcHandle: THandle;
  NextProc      : Boolean;
  TThreadEntry  : TThreadEntry32;
begin
  SnapProcHandle := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); //Takes a snapshot of the all threads
  Result := (SnapProcHandle <> INVALID_HANDLE_VALUE);
  if Result then
  try
    TThreadEntry.dwSize := SizeOf(TThreadEntry);
    NextProc := Thread32First(SnapProcHandle, TThreadEntry);//get the first Thread
    while NextProc do
    begin
      if TThreadEntry.th32OwnerProcessID = PID then //Check the owner Pid against the PID requested
      begin
        Writeln('Thread ID      '+inttohex(TThreadEntry.th32ThreadID,8));
        Writeln('base priority  '+inttostr(TThreadEntry.tpBasePri));
        Writeln('');
      end;

      NextProc := Thread32Next(SnapProcHandle, TThreadEntry);//get the Next Thread
    end;
  finally
    CloseHandle(SnapProcHandle);//Close the Handle
  end;
end;

begin
  { TODO -oUser -cConsole Main : Insert code here }
  GettthreadsList(GetCurrentProcessId); //get the PID of the current application
  //GettthreadsList(5928);
  Readln;
end.
Run Code Online (Sandbox Code Playgroud)

但看起来在这个列表中,它们的一些线程并不是真正由我的代码创建的,并且这些线程永远不会关闭。例如,对于空白项目,这是线程列表:

在此输入图像描述

Dal*_*kar 9

您面临的核心问题并非来自匿名线程本身,而是来自自毁匿名线程 - 已设置的匿名线程FreeOnTerminate

为了等待线程,您需要引用线程或其句柄(Windows 平台)。因为您正在处理自毁线程,所以引用线程不是一个选项,因为一旦启动线程,您就不再允许触摸该引用。

Delphi RTL 在应用程序关闭期间不会对自毁匿名线程执行任何清理,因此在应用程序主窗体被破坏后,这些线程将被操作系统杀死,因此您的问题。

允许您等待匿名线程的解决方案之一,不需要任何复杂的内务处理和维护任何类型的列表,并且还需要对代码进行最小的更改(可以通过简单的查找和替换来完成),即TCountdownEvent使用计算线程数。

这需要替换TThread.CreateAnonymousThread为构造自定义线程类TAnoThread.Create(如果您愿意,您可以添加静态工厂方法,而不是直接调用构造函数),该类将具有与匿名线程相同的行为,除了它的实例将被计数并且您将能够等待所有此类线程完成运行。

type
  TAnoThread = class(TThread)
  protected
    class var
      fCountdown: TCountdownEvent;
    class constructor ClassCreate;
    class destructor ClassDestroy;
  public
    class procedure Shutdown; static;
    class function WaitForAll(Timeout: Cardinal = INFINITE): TWaitResult; static;
  protected
    fProc: TProc;
    procedure Execute; override;
  public
    constructor Create(const aProc: TProc);
  end;

class constructor TAnoThread.ClassCreate;
begin
  fCountdown := TCountdownEvent.Create(1);
end;

class destructor TAnoThread.ClassDestroy;
begin
  fCountdown.Free;
end;

class procedure TAnoThread.Shutdown;
begin
  fCountdown.Signal;
end;

class function TAnoThread.WaitForAll(Timeout: Cardinal): TWaitResult;
begin
  Result := fCountdown.WaitFor(Timeout);
end;

constructor TAnoThread.Create(const aProc: TProc);
begin
  inherited Create(True);
  fProc := aProc;
  FreeOnTerminate := True;
end;

procedure TAnoThread.Execute;
begin
  if fCountdown.TryAddCount then
    try
      fProc;
    finally
      fCountdown.Signal;
    end;
end;
Run Code Online (Sandbox Code Playgroud)

然后您可以在表单析构函数或任何其他适当的位置添加以下代码,并等待所有匿名线程完成运行。

destructor TForm1.Destroy;
begin
  TAnoThread.Shutdown;
  while TAnoThread.WaitForAll(100) <> wrSignaled do
    CheckSynchronize;
  inherited;
end;
Run Code Online (Sandbox Code Playgroud)

原理如下:倒计时事件以值 1 创建,当该值达到 0 时,将发出事件信号。要启动关闭,您可以调用Shutdown将减少初始计数的方法。您不能多次调用此方法,因为它会弄乱计数。

当匿名线程Execute方法启动时,它会首先尝试增加计数。如果它不能做到这一点,则意味着倒计时事件已发出信号,并且线程将终止而不调用其匿名方法,否则匿名方法将运行,并且在完成后计数将减少。

如果匿名线程使用TThread.Synchronize调用,则不能只调用WaitForAll,因为从主线程调用它会死锁。为了防止在等待线程完成时出现死锁,您需要调用CheckSynchronize来处理挂起的同步请求。

该解决方案对TAnoThread类的所有线程进行计数,无论它们是否自毁。这可以很容易地更改为仅计算那些已FreeOnTerminate设置的。

另外,当您调用Shutdown,并且仍然有一些正在运行的线程时,新线程仍然能够在此时启动,因为甚至没有发出倒计时信号。如果您想从那时起阻止新线程,您将需要添加一个布尔标志来指示您已启动关闭过程。

  • “*因为您正在处理自毁线程,所以不能选择引用线程,因为一旦您启动线程,您就不再被允许触摸该引用*” - 如果您分配一个`OnTerminate,则可以在`Start()`之前处理线程。然后你可以保存一个引用,并在线程即将自毁时清除该引用。只是不要在自毁线程上调用`WaitFor()`(它会崩溃),但是如果您需要等待这样的线程,那么您可以在`Start()`之前保存线程的`Handle`,然后您可以在其上使用操作系统等待功能。 (4认同)