Delphi线程异常机制

17

我在 Delphi 中遇到了线程如何工作以及为什么在线程应该引发异常的时候,却没有显示异常的困境。下面是带有注释的代码,也许有人可以向我解释一下该线程或 Delphi 如何处理访问冲突。

//线程代码

unit Unit2;

interface

uses
  Classes,
  Dialogs,
  SysUtils,
  StdCtrls;

type
  TTest = class(TThread)
  private
  protected
    j: Integer;
    procedure Execute; override;
    procedure setNr;
  public
    aBtn: tbutton;
  end;

implementation


{ TTest }

procedure TTest.Execute;
var
  i                 : Integer;
  a                 : TStringList;
begin
 // make severals operations only for having something to do
  j := 0;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;

  Synchronize(setnr);
  a[2] := 'dbwdbkbckbk'; //this should raise an AV!!!!!!

end;

procedure TTest.setNr;
begin
  aBtn.Caption := IntToStr(j)
end;

end.
项目的代码
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs,
  Unit2, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
  public
    nrthd:Integer;
    acrit:TRTLCriticalSection;
    procedure bla();
    procedure bla1();
    function bla2():boolean;
    procedure onterm(Sender:TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.bla;
begin
 try
  bla1;
 except on e:Exception do
   ShowMessage('bla '+e.Message);
 end;
end;

procedure TForm1.bla1;
begin
 try
  bla2
 except on e:Exception do
   ShowMessage('bla1 '+e.Message);
 end;
end;

function TForm1.bla2: boolean;
var ath:TTest;
begin
 try
  ath:=TTest.Create(true);
   InterlockedIncrement(nrthd);
  ath.FreeOnTerminate:=True;
  ath.aBtn:=Button1;
  ath.OnTerminate:=onterm; 
   ath.Resume;
 except on e:Exception do
  ShowMessage('bla2 '+e.Message);
 end;
end;

procedure TForm1.Button1Click(Sender: TObject);

begin
//
 try
   bla;
   while nrthd>0 do
    Application.ProcessMessages;
 except on e:Exception do
  ShowMessage('Button1Click '+e.Message);
 end;
 ShowMessage('done with this');
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
 nrthd:=0;
end;

procedure TForm1.onterm(Sender: TObject);
begin
 InterlockedDecrement(nrthd)
end;

end.

这个应用程序的目的仅是为了知道访问冲突在哪里被捕获,并且如何编写代码。
我不理解为什么在“a [2]:= 'dbwdbkbckbk';”这一行中不会引发访问冲突。


1
调试器没有告诉你关于异常的情况吗? - Rob Kennedy
“a”变量没有被初始化!如果它指向一个有效的内存地址呢?我是说进程所拥有的一个地址。那么你的代码将在那个位置写入,而不会发生访问冲突。我是对的吗?我认为你至少应该将a设为空值(NIL)。 - Gabriel
请查看Remy Lebeau在这里的评论。它解释了AV是什么以及它是如何发生的:https://stackoverflow.com/a/16071764/46207 - Gabriel
4个回答

22
在Delphi 2005中(以及可能大多数其他版本),如果异常从Execute方法中逃逸而没有被处理,则会被调用Execute的函数捕获并存储在线程的FatalException属性中(查看Classes.pas,ThreadProc)。在线程被释放之前,不会对这个异常采取任何进一步的操作,此时异常也将被释放。
因此,您有责任检查该属性并采取相应措施。您可以在线程的OnTerminate处理程序中检查它。如果它不是null,那么线程由于未捕获的异常而终止。例如:
procedure TForm1.onterm(Sender: TObject);
var
  ex: TObject;
begin
  Assert(Sender is TThread);
  ex := TThread(Sender).FatalException;
  if Assigned(ex) then begin
    // Thread terminated due to an exception
    if ex is Exception then
      Application.ShowException(Exception(ex))
    else
      ShowMessage(ex.ClassName);
  end else begin
    // Thread terminated cleanly
  end;
  Dec(nrthd);
end;

不需要使用互锁函数来追踪线程计数。您的线程创建函数和终止处理程序始终在主线程上下文中运行。普通的 IncDec 就足够了。


+1。我从未见过FatalException?...哦,等等,我们还在用Delphi 5。 - Lieven Keersmaekers
我没有那个版本的源代码了,@Lieven。如果它有 AcquireExceptionObject,那么你可以自己模仿新的 FatalException 行为。 - Rob Kennedy
@Lieven:在D5中,以及我认为在D6中也是如此,线程的执行方法尚未受到保护...你必须在Execute的重写中自己完成保护。 - Marjan Venema
1
TThread.FatalException属性(和System.AcquireExceptionObject()函数)在D6中引入。在D5中,Execute()只被一个try..finally包装以确保始终调用DoTerminate(),但线程会在抛出异常之前被终止(通过EndThread()),因此无法将其分派到finally之外的处理程序。在D6中,TThread.Execute()被一个try..except包装,它获取异常并将其存储在FatalException中,然后调用DoTerminate() - Remy Lebeau
1
适用于 Delphi 10.1 Berlin。 - alitrun
感谢@alitrun,自d5以来,每个版本之间都有一些小差异,每个信息片段都必须指定版本...很难知道何时应该使用“未标记版本”的解决方案。 - Darkendorf

13

线程是一个你应该吞下异常的地方。

在处理线程中的异常时,重点是如果你想让终端用户看到异常信息,你应该捕获它并将其传递到主线程,在那里可以安全地显示它。

你可以在这篇EDN线程文章中找到一些例子:如何处理TThread对象中的异常

procedure TMyThread.DoHandleException;
begin
  // Cancel the mouse capture
  if GetCapture <> 0 then SendMessage(GetCapture, WM_CANCELMODE, 0, 0);
  // Now actually show the exception
  if FException is Exception then
    Application.ShowException(FException)
  else
    SysUtils.ShowException(FException, nil);
end;

procedure TMyThread.Execute;
begin
  FException := nil;
  try
    // raise an Exception
    raise Exception.Create('I raised an exception');
  except
    HandleException;
  end;
end;

procedure TMyThread.HandleException;
begin
  // This function is virtual so you can override it
  // and add your own functionality.
  FException := Exception(ExceptObject);
  try
    // Don't show EAbort messages
    if not (FException is EAbort) then
      Synchronize(DoHandleException);
  finally
    FException := nil;
  end;
end;

0

我们也可以重新引发FatalException。重新引发似乎不太合逻辑,但如果您的代码中有一个中央异常/错误处理程序,并且您只想将线程异常包含在该机制中,则可以在某些罕见情况下重新引发:

procedure TForm1.onterm(Sender: TObject);
var
  ex: Exception;
begin
  Assert(Sender is TThread);
  ex := Exception(TThread(Sender).FatalException);
  if Assigned(ex) then
    // Thread terminated due to an exception
    raise ex;
  Dec(nrthd);
end;

1
你确定这个能正常工作吗?释放异常对象怎么办? - dummzeuch
2
这个不起作用(至少在XE2中是这样)。我尝试了一下,得到了一个非模态的消息对话框,然后是一个EOSError。 - Jens Mühlenhoff
1
OnTerminate 处理程序在 Execute() 退出后,由工作线程上下文中的 Synchronize() 调用。如果在 Synchronize() 内部引发异常,则会获取该异常并在工作线程上下文中重新引发它。因此,在 OnTerminate 中引发异常是不好的。要正确执行此操作,您需要手动拥有 FatalException,以某种方式重置 TThread.FFatalException 为 nil(以便 TThread 不销毁对象),并在 Synchronize() 退出后自己在主线程中重新引发它。需要使用一些技巧来拥有所有权。 - Remy Lebeau

0

也许你展示的例子并不是最好的,因为“a”变量没有初始化!它可以指向计算机中任何可能的内存位置。它甚至可以指向不存在物理上存在的位置(尽管由于虚拟内存系统,这是无意义的)。

因此,在您的程序中,如果“a”偶然指向一个有效的内存地址(我的意思是进程拥有的地址),那么您的代码将在没有访问冲突的情况下写入该位置。我认为您应该至少将“a”设置为NIL。之后,请查看Lieven Keersmaekers的帖子。

请参见Remy Lebeau在此处的评论:http://www.stackoverflow.com/a/16071764/46207
还有这个:Why uninitialized pointers cause mem access violations close to 0?


问题的原因是众所周知的。实际上,故意在这个问题中包含了关于a的问题,以演示在Execute方法中触发异常的情况。对于异常的原因不需要任何指导。问题是为什么该异常没有像其他异常一样被报告。将a赋值为nil也不会改变任何事情。 - Rob Kennedy
只要a[2]指向随机内存地址,就不能保证触发异常。在极少数情况下,程序可能会成功写入a[2]。为了演示触发异常。 - Gabriel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接