c00000005 ACCESS_VIOLATION 在线程运行时退出应用程序

问题描述 投票:0回答:2

c00000005 ACCESS_VIOLATION 在以下线程运行时关闭表单(退出应用程序)时引起(这只是一个示例,以说明问题):

type
  Twtf = class(TThread)
  protected
    procedure Execute; override;
  end;

procedure Twtf.Execute;
const
  hopefully_big_enough_to_trigger_problem = 100000000;
var
  A: array of Integer;
  I: Integer;
begin
  SetLength(A, hopefully_big_enough_to_trigger_problem);
  while not Terminated do begin
    for I := Low(A) to High(A) do
      A[I] := 0;
  end;
end;

var
  wtf: Twtf;

procedure TForm1.Button4Click(Sender: TObject);
begin
  wtf := Twtf.Create;
  wtf.FreeOnTerminate := True;
end;

根据局部变量调试窗口发生的情况是

A
现在是
()
,即Length(A)为0。显然这就是访问
A[I]
导致问题的原因,但请注意我没有手动做任何事情到
A
.

如何阻止显示此错误(不一定要阻止发生)?应用程序正在关闭,线程应该就这样死掉了……安静地。这实际上发生在较小的阵列上,但如果足够大,每 3 次尝试中有 1 次或 2 次会导致问题(如果运行调试它总是显示;即,这总是发生,但有时只是隐藏)。

以下内容无济于事:

destructor TForm1.Destroy;
begin
  wtf.terminate;
  inherited;
end;

只有在析构函数中执行

TerminateThread(wtf.Handle, 0);
才能解决(隐藏)问题。但肯定有更优雅的方式吗?

multithreading delphi access-violation
2个回答
3
投票

一旦你开始自毁线程,你就会严重限制你管理它和执行所需的清理和等待的能力。它们作为需要运行相当小的任务的即发即弃线程很方便,任务可以随时安全地中断(杀死)。

如果任务需要清理,那么你不应该使用自毁线程。

除了应用程序关闭期间发生的 AV,您的代码还有其他问题。


以下代码不是初始化自毁线程的正确方法:

wtf := Twtf.Create;
wtf.FreeOnTerminate := True;

在上面的代码中,线程将在构造后立即开始运行,并且有可能(尽管不太可能)线程将完成其工作,然后您才能将

FreeOnTerminate
标志设置为 true,这会造成内存泄漏。

正确的做法是在挂起模式下创建线程,然后设置

FreeOnTerminate
然后启动一个线程:

wtf := Twtf.Create(True);
wtf.FreeOnTerminate := True;
wtf.Start;

另一种方法是覆盖

TThread.Create
并在那里设置
FreeOnTerminate
标志。


下一个问题是,在你启动自毁线程后,你永远不应该访问它的引用。所以你永远不能调用

wtf.Terminate
,也不能调用
TerminateThread(wtf.Handle, 0);
,因为那时线程可能已经被销毁,
wtf
将成为指向不存在实例的悬空引用。

因为你不能调用

wtf.Terminate
while not Terminated
循环也是无用的,除非你将构建你将手动管理的线程类的其他实例。


有很多方法可以解决你的问题,但是你选择哪一种取决于线程的实际工作以及你需要运行多少个这样的线程以及你是否可以随意中断(终止)它们或者你需要等待它们完成。

如果你可以安全地中断线程正在做的任何事情并且显示 AV 是你唯一的问题,那么你可以用

Execute
将代码包装在
try...except
方法中并吃掉异常。

最好的选择是使用手动管理的线程,你将创建一次并在 for 关闭时销毁。在这种情况下,

while not Terminated
循环是有意义的,因为它允许线程在应用程序关闭时被中断:

 procedure TForm1.Button4Click(Sender: TObject);
 begin
   wtf := Twtf.Create;
 end;

destructor TForm1.Destroy;
begin
  // calling Free will terminate thread and wait for it
  wtf.Free; 
  inherited;
end;

当然,还有其他管理线程的选项,无论它们是自毁的还是手动管理的,但不可能一一列举,尤其是当您的特定用例不明确时。但是所有这些都需要一些额外的机制来向线程发出信号,表明它需要停止其工作或应用程序停止其线程仍在运行。

根据您使用的 Delphi 版本,您可能还想看看

TTask
因为运行和处理任务比处理线程更容易,并且用于运行任务的线程由 RTL 自动管理。


补充阅读:

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

我可以从 VCL 线程调用 TerminateThread 吗?


0
投票

重申:

这是一个示例,旨在说明当应用程序终止时线程循环正在访问动态数组时会发生什么(无论任何优雅的关闭机制,线程可能在那个时间点在循环内,并在同时阻塞应用程序由于明显的原因,可能等待很长时间才能完成或进行

if Terminated
检查并不是最佳选择。

问题是由于应用程序在释放线程之前释放了数组的内存,而线程正在处理数组(数组是属于线程的变量);例如,对于足够大的多维

A: array of array of Integer
,实际上有可能看到
A[500000]
被释放到
()
A[400000]
仍然很好。

解决方案简单到微不足道:

Twtf = class(TThread)

...

var
  wtf: Twtf;

...

destructor TForm1.Destroy;
begin
  // Only Form in the Application, otherwise add Application.Terminated check?
  if wtf <> nil then wtf.Suspend;
  inherited;
end;

这消除了处理其数组的线程与释放该数组的应用程序之间的竞争条件。

以上所有与

FreeOnTerminate
无关。

© www.soinside.com 2019 - 2024. All rights reserved.