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

m3eecexj  于 2023-03-01  发布在  其他
关注(0)|答案(2)|浏览(301)

c 00000005 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;

根据Local Variables调试窗口,A现在是(),即Length(A)是0。显然,这就是访问A[I]会导致问题的原因,但请注意,我没有手动对A执行任何操作。
我如何停止显示这个错误(不一定要防止发生)?应用程序正在关闭,线程应该只是死...安静。这实际上发生在较小的数组,但有一个足够大的1或2每3次尝试导致问题(如果运行调试它总是显示;即这总是发生,但有时简单地隐藏)。
以下内容没有帮助:

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

相反,只在析构函数中执行TerminateThread(wtf.Handle, 0);就解决(隐藏)了问题,但肯定有更优雅的方法吗?

wsewodh2

wsewodh21#

一旦你启动了自毁线程,你就严重限制了你管理它和执行所需的清理和等待的能力。它们作为需要运行相当小的任务的"发射和遗忘"线程是很方便的,而这些任务可以在任何时候被安全地中断(杀死)。
如果任务需要清理,则不应使用自销毁线程。
除了在应用程序关闭期间发生的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是您唯一的问题,那么您可以使用try...except Package Execute方法中的代码,并吃掉异常。
最好的选择是使用手动管理的线程,您将创建一次,并在窗体关闭时销毁。在这种情况下,while not Terminated循环是有意义的,因为它将允许线程在应用程序关闭时中断。您也可以在应用程序关闭过程的早期调用wtf.Terminate(例如,在OnClose中,或OnCloseQuery以给予线程更多时间来检查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自动管理的。
如果您真的、真的、真的坚持使用自毁线程,并且只想在uses关闭应用程序后尽快终止所有线程,那么您可以使用TerminateProcess。请注意,如果您的应用程序使用DLL或共享内存,则通过调用TerminateThreadExitProcessTerminateProcess突然终止线程或进程可能会导致问题。请参见:TerminateProcess functionTerminating a Process
要做到这一点,您可以在主窗体中添加以下析构函数:

destructor TForm1.Destroy;
begin
  inherited;
  TerminateProcess(GetCurrentProcess, 0);
end;

补充阅读:
How to wait that all anonymous thread are terminated before closing the app?
Can I call TerminateThread from a VCL thread?

k4aesqcs

k4aesqcs2#

重申:
此示例旨在说明在应用程序终止时线程循环正在访问动态数组时会发生什么情况(无论采用何种正常关闭机制,线程都可能在该时间点处于循环中,并且阻塞应用程序,等待它完成或进入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
    with wtf do
      if not Finished then
        Suspend;
  inherited;
end;

这消除了处理其数组的线程与释放该数组的应用程序之间的争用情况。
以上所有都与FreeOnTerminate无关。
编辑:添加Finished检查
EDIT:弃用暂停的替代选项:SuspendThread(Handle);

相关问题