终止一个 'sleeping' 线程
Terminate a 'sleeping' thread
我有一个任务想在后台运行并且不中断 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.
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.
您不能中断 Sleep()
,因此这不是调试的好选择,尤其是对于长间隔。您应该改用 FDelayEvent.WaitFor()
。这样,线程在终止时可以快速“唤醒”。
此外,不要在 Execute()
结束时调用 Synchronize()
,您应该使用 OnTerminate
事件代替线程完成时需要的任何操作。 OnTerminate
已经与主 UI 线程同步。
例如,当线程终止时,您可以使用 OnTerminate
将 AThread
变量设置为 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.
如果:
- 无论出于何种原因,您想保留代码的结构...
- 当你说“如果应用程序在任何时候关闭,任何剩余的线程都应该尽快终止。” -> 这意味着我们并不关心关于线程 run/worked 任何进程的结果 ...
然后:
- 这不会是一种“干净”,学术、软件工程的方式
- 这只是完成任务的替代方法(针对 2 个目标:“杀死”/终止线程并避免内存泄漏)
将您的代码行保留在下方。
FreeOnTerminate := False;
“杀死”/终止线程(无论其状态如何)。
TerminateThread(AThread.Handle, 0);
TForm1
has/owns AThread
,因为您的代码可能会重新创建 AThread
(在 FormCreate
然后在 Button1Click
),每次线程终止后立即释放线程(目前,您在 Button1Click
和 FormClose
中执行此操作)。以防万一你在其他地方做 if Assigned(AThread)
,使用 FreeAndNil
(它释放 AThread
并将其设置为 nil
,这样 Assigned(AThread)
可以正确地 return False
当使用您的代码结构终止并释放线程时)。
FreeAndNil(AThread);
TerminateThread
方法(上面)需要 Winapi.Windows
(您在 Unit1
的使用列表中已经有了)。它取代了 AThread.Terminate
.
它看起来像下面这样:
if Assigned(AThread) then begin
TerminateThread(AThread.Handle, 0);
FreeAndNil(AThread);
end;
我有一个任务想在后台运行并且不中断 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.
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.
您不能中断 Sleep()
,因此这不是调试的好选择,尤其是对于长间隔。您应该改用 FDelayEvent.WaitFor()
。这样,线程在终止时可以快速“唤醒”。
此外,不要在 Execute()
结束时调用 Synchronize()
,您应该使用 OnTerminate
事件代替线程完成时需要的任何操作。 OnTerminate
已经与主 UI 线程同步。
例如,当线程终止时,您可以使用 OnTerminate
将 AThread
变量设置为 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.
如果:
- 无论出于何种原因,您想保留代码的结构...
- 当你说“如果应用程序在任何时候关闭,任何剩余的线程都应该尽快终止。” -> 这意味着我们并不关心关于线程 run/worked 任何进程的结果 ...
然后:
- 这不会是一种“干净”,学术、软件工程的方式
- 这只是完成任务的替代方法(针对 2 个目标:“杀死”/终止线程并避免内存泄漏)
将您的代码行保留在下方。
FreeOnTerminate := False;
“杀死”/终止线程(无论其状态如何)。
TerminateThread(AThread.Handle, 0);
TForm1
has/ownsAThread
,因为您的代码可能会重新创建AThread
(在FormCreate
然后在Button1Click
),每次线程终止后立即释放线程(目前,您在Button1Click
和FormClose
中执行此操作)。以防万一你在其他地方做if Assigned(AThread)
,使用FreeAndNil
(它释放AThread
并将其设置为nil
,这样Assigned(AThread)
可以正确地 returnFalse
当使用您的代码结构终止并释放线程时)。FreeAndNil(AThread);
TerminateThread
方法(上面)需要 Winapi.Windows
(您在 Unit1
的使用列表中已经有了)。它取代了 AThread.Terminate
.
它看起来像下面这样:
if Assigned(AThread) then begin
TerminateThread(AThread.Handle, 0);
FreeAndNil(AThread);
end;