一些线程在应用程序退出时仍在运行是否有问题?
字面意义上理解这个问题有点不完整。这是因为在默认情况下,Delphi 应用程序结束后会调用 ExitProcess
,此时没有线程在运行。
回答“一些线程没有完成是否有问题”的问题取决于这些线程未能完成的内容。您需要仔细分析线程代码,但一般来说,这可能容易出错。
Windows会自动清理我的内存,就这样了吗?在IDE中我会收到未释放对象的通知,但在IDE外看起来就像是平静的。
当进程地址空间被销毁时,操作系统将回收已分配的内存,当进程句柄表被销毁时,所有对象句柄将被关闭,所有加载的库的入口点将被调用,并带有 DLL_PROCESS_DETACH
。我找不到任何关于此的文档,但我也认为挂起的IO请求将被调用以取消。
但这并不意味着没有问题。例如,涉及进程间通信或同步对象时,情况可能会变得混乱。
文档中的
ExitProcess
详细介绍了一个例子:如果一个线程在分离时消失前未释放一个库试图获取的锁,则会出现死锁。
此博客文章提供了另一个具体示例,在该示例中,如果一个线程尝试进入已被另一个已终止的线程孤立的临界区域,则操作系统会强制终止退出进程。
虽然在退出时放弃资源释放可能是有道理的,特别是如果清理需要相当长的时间,但对于一个非平凡应用程序来说,这样做可能会出现问题。一个健壮的策略是在调用
ExitProcess
之前清理所有内容。另一方面,如果你发现自己处于已经调用
ExitProcess
的情况下,例如进程由于终止而从你的 dll 中分离,那么唯一安全的做法就是将所有东西留下并返回 - 每个其他的 dll 都可能已经被卸载,每个其他的线程都已经终止。
在这种情况下,使用TerminateThread终止这样简单的线程是否可能是有害的?
TerminateThread
建议仅在极端情况下使用,但由于问题有一个加粗的“THIS”,应该检查代码的实际作用。查看RTL代码,我们可以看到最糟糕的情况是留下一个文件句柄,只用于读取访问。在进程终止时,这不是问题,因为句柄很快就会关闭。
我通常从OnTerminate()事件的线程结果中获取并在之后使用FreeOnTerminate释放这些线程。如果我想自己释放它们,那么应该在什么时候释放呢?
唯一严格的规则是在它们执行完成之后。选择可能会受到应用程序设计的指导。不同的是,您将无法使用FreeOnTerminate,并且您将保留对线程的引用以便能够释放它们。在我为回答这个问题而工作的测试用例中,已完成的工作线程在定时器触发时被释放,有点像垃圾收集器。
我可以在OnTerminate事件中释放一个线程吗?还是说这有点太早了?
在对象的一个事件处理程序中释放该对象会导致操作已释放实例内存的风险。文档 特别警告组件不要这样做,但总体来说,这适用于所有类。
即使您想忽略警告,这也会造成死锁。虽然处理程序在 Execute
返回后调用,但 OnTerminate
仍然从 ThreadProc 同步。如果您尝试在处理程序中释放线程,它将导致主线程等待线程完成 - 而线程正在等待主线程从 OnTerminate
返回,这是一个死锁。
如果不使用OnTerminate,线程将如何通知我它已完成?
OnTerminate
可以用于通知线程已完成其工作,但您也可以使用其他方式,如使用同步对象、排队过程或发布消息等。值得注意的是,可以等待线程句柄,这就是 TThread.WaitFor
的作用。
在我的测试程序中,我尝试确定应用程序终止时间,这取决于各种退出策略。所有的测试结果都依赖于我的测试环境。
终止时间从VCL表单的OnClose处理程序被调用开始计算,到RTL调用ExitProcess之前结束。此外,该方法不考虑ExitProcess花费的时间,我认为当有悬挂线程时会有所不同。但无论如何,我都没有尝试去衡量它。
工作线程查询不存在主机上的目录。这是我能想到的等待时间最长的情况。每个查询都在一个新的不存在的主机上,否则DirectoryExists会立即返回。
一个定时器启动并收集工作线程。根据IO查询所需的时间(大约为550ms),定时器间隔影响任何给定时间的线程总数。我使用250ms的定时器间隔测试了大约10个线程。
各种调试输出允许在IDE的事件日志中跟踪流程。
我的第一个测试是放弃工作线程 - 只是退出应用程序。我测量的时间是30-65毫秒。同样,这可能导致 ExitProcess
本身需要更长的时间。
接下来,我使用 TerminateThread
终止线程。这需要140-160毫秒。我认为,如果可以计算出 ExitProcess
的时间,那么上一个测试结果实际上更接近这个时间。但我没有证据证明这一点。
接下来,我测试了在运行线程上取消IO请求,然后将它们留在后面。这显着减少了泄漏内存的数量,在大多数情况下完全消除了泄漏。虽然取消请求是异步的,但几乎所有的线程都会立即返回并找到时间来完成。无论如何,这需要160-190毫秒。
我应该在这里指出,DirectoryExists
中的代码存在缺陷,至少在XE2中是如此。函数要做的第一件事就是调用 GetFileAttributes
。如果返回值是 INVALID_FILE_ATTRIBUTES
,则表示函数失败。这是RTL处理失败的方式:
function DirectoryExists(const Directory: string; FollowLink: Boolean = True): Boolean;
...
...
Result := False;
Code := GetFileAttributes(PChar(Directory));
if Code <> INVALID_FILE_ATTRIBUTES then
begin
...
end
else
begin
LastError := GetLastError;
Result := (LastError <> ERROR_FILE_NOT_FOUND) and
(LastError <> ERROR_PATH_NOT_FOUND) and
(LastError <> ERROR_INVALID_NAME) and
(LastError <> ERROR_BAD_NETPATH);
end;
end;
此代码假定除非
GetLastError
返回上述错误代码之一,否则目录存在。这种推理是有缺陷的。事实上,当您取消IO请求时,
GetLastError
返回文档中的
ERROR_OPERATION_ABORTED
(995),但是无论目录是否存在,
DirectoryExists
都返回 true。
我使用的测试代码如下:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes,
Vcl.Controls, Vcl.Forms, Vcl.ExtCtrls,
generics.collections;
type
TForm1 = class(TForm)
Timer1: TTimer;
procedure Timer1Timer(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure FormDestroy(Sender: TObject);
private
FThreads: TList<TThread>;
end;
var
Form1: TForm1;
implementation
uses
diagnostics;
{$R *.dfm}
type
TIOThread = class(TThread)
private
FTarget: string;
protected
constructor Create(Directory: string);
procedure Execute; override;
public
destructor Destroy; override;
end;
constructor TIOThread.Create(Directory: string);
begin
FTarget := Directory;
inherited Create;
end;
destructor TIOThread.Destroy;
begin
inherited;
OutputDebugString(PChar(Format('Thread %d destroyed', [ThreadID])));
end;
procedure TIOThread.Execute;
var
Watch: TStopwatch;
begin
OutputDebugString(PChar(Format('Thread Id: %d executing', [ThreadID])));
Watch := TStopwatch.StartNew;
ReturnValue := Ord(DirectoryExists(FTarget));
Watch.Stop;
OutputDebugString(PChar(Format('Thread Id: %d elapsed time: %dms, return: %d',
[ThreadID, Watch.Elapsed.Milliseconds, ReturnValue])));
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FThreads := TList<TThread>.Create;
Timer1.Interval := 250;
Timer1.Enabled := True;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FThreads.Free;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
ShareName: array [0..12] of Char;
i: Integer;
H: THandle;
begin
for i := FThreads.Count - 1 downto 0 do
if FThreads[i].Finished then begin
FThreads[i].Free;
FThreads.Delete(i);
end;
for i := Low(ShareName) to High(ShareName) do
ShareName[i] := Chr(65 + Random(26));
FThreads.Add(TIOThread.Create(Format('\\%s\share', [string(ShareName)])));
OutputDebugString(PChar(Format('Possible thread count: %d', [FThreads.Count])));
end;
var
ExitWatch: TStopwatch;
function CancelSynchronousIo(hThread: THandle): Bool; stdcall; external kernel32;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var
i: Integer;
Handles: TArray<THandle>;
IOPending: Bool;
Ret: DWORD;
begin
ExitWatch := TStopwatch.StartNew;
Timer1.Enabled := False;
if FThreads.Count > 0 then begin
SetLength(Handles, FThreads.Count);
for i := 0 to FThreads.Count - 1 do
Handles[i] := FThreads[i].Handle;
OutputDebugString(PChar(Format('Cancelling at most %d threads', [Length(Handles)])));
for i := 0 to Length(Handles) - 1 do
if GetThreadIOPendingFlag(Handles[i], IOPending) and IOPending then
CancelSynchronousIo(Handles[i]);
Assert(FThreads.Count <= MAXIMUM_WAIT_OBJECTS);
OutputDebugString(PChar(Format('Will wait on %d threads', [FThreads.Count])));
Ret := WaitForMultipleObjects(Length(Handles), @Handles[0], True, INFINITE);
case Ret of
WAIT_OBJECT_0: OutputDebugString('wait success');
WAIT_FAILED: OutputDebugString(PChar(SysErrorMessage(GetLastError)));
end;
for i := 0 to FThreads.Count - 1 do
FThreads[i].Free;
end;
end;
procedure Exiting;
begin
ExitWatch.Stop;
OutputDebugString(PChar(
Format('Total exit time:%d', [ExitWatch.Elapsed.Milliseconds])));
end;
initialization
ReportMemoryLeaksOnShutdown := True;
ExitProcessProc := Exiting;
end.
WaitForMultipleObjects()
在设置了关闭事件时退出线程。 - mghie