【问题标题】:Terminate a 'sleeping' thread终止“睡眠”线程
【发布时间】:2020-07-31 07:20:00
【问题描述】:

我有一个任务要在后台运行,而不是中断 GUI 线程来检查新的程序版本。

当应用程序启动时,它立即将一个线程排队等待 15 秒,然后执行剩余的代码。如果检查是在 15 秒结束之前手动触发的,则应该终止现有的自动线程。如果应用程序在任何时候关闭,任何剩余的线程都应尽快终止。

为了便于调试,我在下面的示例中使用了 2 分钟。

我现在的问题是,我尝试使用 WaitFor、Terminate、FreeOnTerminate 和 OnTerminated 的任何组合都不会获得预期的结果。要么 Destroy 没有被调用,我得到内存泄漏,应用程序在终止线程时挂起,或者我得到 Cannot terminate externally created thread 异常。

线程代码

unit unCheckThread;

interface

uses SysUtils, Classes, SyncObjs, Dialogs;

type
  TCheckThread = class(TThread)
  private
    FDelayEvent: TEvent;
    FDelay: Integer;
  public
    constructor Create(const ADelay: Integer);
    destructor Destroy; override;
    procedure Execute; override;
    procedure TerminatedSet; override;
  end;

implementation

{ TCheckThread }

constructor TCheckThread.Create(const ADelay: Integer);
begin
  inherited Create(True);
  FreeOnTerminate := False;

  FDelay := ADelay;
  FDelayEvent := TEvent.Create(nil, True, False, '');
end;

destructor TCheckThread.Destroy;
begin
  FDelayEvent.Free;

  inherited;
end;

procedure TCheckThread.Execute;
begin
  FDelayEvent.WaitFor(MSecsPerSec * FDelay);

  { if another thread has checked while waiting for the delay, cancel this check }
  if Terminated then Exit;

  { some long running code }
  Sleep(10000);

  if Terminated then Exit;

  Synchronize(
    procedure()
    begin
      MessageDlg('Thread completed', mtConfirmation, [mbOK], 0);
    end);
end;

procedure TCheckThread.TerminatedSet;
begin
  FDelayEvent.SetEvent;
end;

end.

用户界面

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, unCheckThread, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    AThread: TCheckThread;
    procedure onterminate(Sender: TObject);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  if Assigned(AThread) then begin
    AThread.Terminate;
//    AThread.WaitFor; // ?
  end;

  AThread := TCheckThread.Create(0); // start immediately
  AThread.OnTerminate := onterminate;
  AThread.Start;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  Close;
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  if Assigned(AThread) then begin
    AThread.Terminate;
//    AThread.WaitFor; // ??
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  AThread := TCheckThread.Create(SecsPerMin * 2); // wait for 2 mintues before starting
end;

procedure TForm1.onterminate(Sender: TObject);
begin
  FreeAndNil(AThread);
end;

end.

【问题讨论】:

  • 1. 使用FormDestroy 而不是FormClose。 2. 删除OnTerminate 事件处理程序。 3. 在FormDestroy 和Button1Click 中都使用FreeAndNil(AThread) 而不是请求终止和等待线程。从主线程中释放线程将为您调用适当的例程。 4. 不要让胎面休眠。它将无法响应取消。您已经使用手动重置事件作为取消初始延迟的信号。稍后再做同样的事情 - 定期检查事件的状态。
  • @PeterWolf "而不是请求终止并等待线程使用 FreeAndNil(AThread)" - 这是非常糟糕的建议。 Free 运行 TThread 是不安全的。在FreeTThread 之前,始终明确 请求终止并等待终止完成。这是安全的方式。
  • @RemyLebeau 感谢您的调整。我知道Free'ing 正在运行的线程可能会导致一些问题。另一方面,Destroy 比Terminate 和WaitFor 线程做得更多。它还有Resume 的暂停线程和Yield 直到它启动,如果它没有启动。因此Terminate & WaitFor 在所有情况下都不够用。我有兴趣进一步阅读如何在 Delphi 中优雅地关闭线程以及什么是陷阱。最值得注意的是,它在调用 inherited 之前在重写的析构函数中破坏了线程的字段,我会说。

标签: delphi delphi-xe2


【解决方案1】:

您不能中断Sleep(),因此这不是调试的好选择,尤其是对于长时间间隔。您应该改用FDelayEvent.WaitFor()。这样,线程可以在被终止时快速“唤醒”。

另外,不要在Execute() 结束时调用Synchronize(),而应该使用OnTerminate 事件来代替线程完成时所需的任何操作。 OnTerminate 已经与主 UI 线程同步。

例如,当线程终止时,您可以使用OnTerminate 将您的AThread 变量设置为nil,这样您的Assigned(AThread) 检查就不会失败。您已经在尝试执行此操作,但是您不能安全地 Free 来自其 OnTerminate 事件处理程序中的线程,因此您可以考虑改用 FreeOnTerminate=True,或者至少延迟Free 直到处理程序退出。

就此而言,我不建议一开始就在应用启动时创建延迟线程。请改用TTimer。当其OnTimer 事件被触发时,THEN 创建一个非延迟线程。如果用户触发手动检查,只需禁用计时器。这样一来,您就不会浪费资源创建一个闲置的线程,也不必担心多个线程彼此同步。

话虽如此,请尝试更多类似的东西:

unit unCheckThread;

interface

uses
  Classes;

type
  TCheckThread = class(TThread)
  protected
    procedure Execute; override;
  public
    constructor Create(AOnTerminate: TNotifyEvent);
  end;

implementation

uses
  SysUtils;

{ TCheckThread }

constructor TCheckThread.Create(AOnTerminate: TNotifyEvent);
begin
  inherited Create(False);
  FreeOnTerminate := False;
  OnTerminate := AOnTerminate;
end;

procedure TCheckThread.Execute;
var
  I: Integer;
begin
  { some long running code }
  for I := 1 to 20 do
  begin
    if Terminated then Exit;
    Sleep(500);
  end;
end;

end.
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, unCheckThread;

const
  WM_FREE_THREAD = WM_APP + 1;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Timer1: TTimer;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    { Private declarations }
    AThread: TCheckThread;
    procedure ThreadFinished(Sender: TObject);
    procedure StartThread;
    procedure StopThread;
    procedure WMFreeThread(var Message: TMessage); message WM_FREE_THREAD;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  StartThread;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  Close;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Timer1.Interval := 120000; // wait for 2 minutes before starting
  Timer1.Enabled = True;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  StopThread;
end;

procedure TForm1.StartThread;
begin
  Timer1.Enabled := False;
  if not Assigned(AThread) then
  begin
    AThread := TCheckThread.Create(ThreadFinished);
    Button1.Enabled := False;
  end;
end;

procedure TForm1.StopThread;
begin
  if Assigned(AThread) then
  begin
    AThread.OnTerminate := nil;
    AThread.Terminate;
    AThread.WaitFor;
    FreeAndNil(AThread);
  end;
end;

procedure TForm1.ThreadFinished(Sender: TObject);
begin
  AThread := nil;

  // in 10.2 Tokyo and later, you can use TThread.ForceQueue() instead...
  // TThread.ForceQueue(nil, Sender.Free);
  PostMessage(Handle, WM_FREE_THREAD, 0, LPARAM(Sender));

  MessageDlg('Thread completed', mtConfirmation, [mbOK], 0);
  Button1.Enabled := True;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  Timer1.Enabled := False;
  StartThread;
end;

procedure TForm1.WMFreeThread(var Message: TMessage);
begin
  TObject(Message.LParam).Free;
end;

end.

【讨论】:

  • 我会等着回去工作来测试这个,但这绝对是正确的方法。我没有考虑过计时器,但这是一个非常明显的简单修复
  • 这是一个很大的帮助,谢谢。您是否能够详细说明为什么无法在OnTerminate 处理程序中安全地释放线程?我已经在OnTerminate 中更改了您的建议FreeOnTerminate=True 和AThread=nil,但为了我自己的利益,我尝试了AThread.Free,它只是在线程终止时冻结应用程序。
  • @Ranky 您无法在其OnTerminate 事件中释放线程,原因与您无法在其自己的事件中释放任何对象的原因相同——RTL 在事件发生后仍需要访问该对象处理程序退出,所以如果你释放它,就会发生坏事。对于TThread 来说更是如此,因为它的析构函数将等待线程终止(如果它还没有终止),因此在其OnTerminate 事件中释放它会导致死锁。
【解决方案2】:

如果:

  • 无论出于何种原因,您都希望保留代码的结构...
  • 当你说“如果应用程序在任何时候关闭,任何剩余的线程都应该尽快终止。” -> 这意味着我们并不真正关心任何结果的结果线程正在运行/处理的进程...

然后:

  • 这不会是一种“干净”、学术、软件工程的方式
  • 这只是简单地完成任务的替代方法(针对 2 个目标:“杀死”/终止线程并避免内存泄漏)
  1. 将您的代码行保留在下面。

    FreeOnTerminate := False;

  2. “杀死”/终止线程(无论其状态如何)。

    TerminateThread(AThread.Handle, 0);

  3. TForm1 拥有/拥有AThread,并且由于您的代码可能会重新创建AThread(在FormCreate 中然后在Button1Click 中),因此每次在线程终止后立即释放线程(当前,你在Button1Click 和FormClose 中执行此操作)。以防万一您在其他任何地方执行if Assigned(AThread),请使用FreeAndNil(它释放AThread并将其设置为nil,这样Assigned(AThread)可以在线程已终止并使用您的释放时正确返回False代码结构)。

    FreeAndNil(AThread);

TerminateThread 方法(上图)需要Winapi.Windows(您在Unit1 的使用列表中拥有)。它取代了AThread.Terminate。

如下所示:

  if Assigned(AThread) then begin
    TerminateThread(AThread.Handle, 0);
    FreeAndNil(AThread);
  end;

【讨论】:

  • 这绝对是错误的做法。当有更好的解决方案时,永远不要求助于TerminateThread()。甚至微软也建议远离TerminateThread(),他们发明了它。当然,在这个例子中,它是相当无害的,但是随着代码的发展和线程实际工作,使用TerminateThread() 变得更加危险。
猜你喜欢
  • 1970-01-01
  • 2015-06-28
  • 2018-08-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-14
  • 1970-01-01
  • 2012-08-08
相关资源
最近更新 更多