终止“睡眠”线程

时间:2020-07-31 07:20:00

标签: delphi delphi-xe2

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

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

在下面的示例中,我使用2分钟进行调试。

我现在的问题是,我尝试使用WaitForTerminateFreeOnTerminateOnTerminated进行的组合不会获得期望的结果。没有调用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.

UI

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.

2 个答案:

答案 0 :(得分:4)

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

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

例如,当该线程终止时,您可以使用OnTerminateAThread变量设置为nil,以使Assigned(AThread)检查不会失败。您已经在尝试这样做,但是您不能从其Free事件处理程序中安全地 OnTerminate线程,因此您可以考虑使用FreeOnTerminate=True来代替,或至少将Free延迟到处理程序退出之后。

因此,我不建议在应用程序启动时创建延迟线程。请改用TTimer。触发其OnTimer事件时,然后创建一个非延迟线程。如果用户触发手动检查,只需禁用计时器。这样,您就不会浪费资源来创建仅处于空闲状态的线程,也不必担心将多个线程彼此同步。

话虽如此,请尝试以下类似操作:

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.

答案 1 :(得分:1)

如果:

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

然后:

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

    FreeOnTerminate := False;

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

    TerminateThread(AThread.Handle, 0);

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

    False

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

如下所示:

AThread.Terminate