3

我写了一个TThread后代类,如果引发异常,将异常的 Class 和 Message 保存在两个私有字段中

private
  //...
  FExceptionClass: ExceptClass;  // --> Class of Exception
  FExceptionMessage: String;
  //...

我以为我可以raise在事件中出现类似的异常OnTerminate,以便主线程可以处理它(这是一个简化版本):

procedure TMyThread.Execute;
begin
  try
    DoSomething;
    raise Exception.Create('Thread Exception!!');
  except
    on E:Exception do
    begin
      FExceptionClass := ExceptClass(E.ClassType);
      FExceptionMessage := E.Message;
    end;
  end;
end;

procedure TMyThread.DoOnTerminate(Sender: TObject);
begin
  if Assigned(FExceptionClass) then
    raise FExceptionClass.Create(FExceptionMessage);
end;

我希望发生标准异常处理机制(一个错误对话框),但我得到的结果好坏参半:出现对话框但随后出现系统错误,或者(更有趣)出现对话框但调用线程的函数运行就好像从未引发异常一样。
我想问题出在调用堆栈上。
这是个坏主意吗?
是否有另一种方法可以将线程异常与主线程分离,但以标准方式重现它们?
谢谢

4

4 回答 4

6

在我看来,这个问题的根本问题是:

当您在线程的OnTerminate事件处理程序中引发异常时会发生什么。

在主线程上调用线程的OnTerminate事件处理程序,通过调用Synchronize. 现在,您的OnTerminate事件处理程序正在引发异常。所以我们需要弄清楚异常是如何传播的。

如果您检查OnTerminate事件处理程序中的调用堆栈,您将看到它是在主线程上从CheckSynchronize. 相关的代码是这样的:

try
  SyncProc.SyncRec.FMethod; // this ultimately leads to your OnTerminate
except
  SyncProc.SyncRec.FSynchronizeException := AcquireExceptionObject;
end;

因此,CheckSynchronize捕获您的异常并将其隐藏在FSynchronizeException. 执行然后继续,然后FSynchronizeException被提升。事实证明,隐藏的异常是在TThread.Synchronize. 最后的死亡行为TThread.Synchronize是:

if Assigned(ASyncRec.FSynchronizeException) then 
  raise ASyncRec.FSynchronizeException;

这意味着您试图在主线程中引发异常的尝试已被框架阻止,该框架将其移回您的线程。现在,这是一场灾难,因为在raise ASyncRec.FSynchronizeException执行时,在这种情况下,没有异常处理程序处于活动状态。这意味着线程过程将抛出一个 SEH 异常。这将使房子倒塌。

所以,我从这一切中得出的结论是以下规则:

      OnTerminate永远不要在线程的事件处理程序中引发异常。

您必须找到一种不同的方式在主线程中显示此事件。例如,将消息排队到主线程,例如通过调用PostMessage.


顺便说一句,您不需要在Execute方法中实现异常处理程序,因为TThread已经这样做了。

的实现TThread将调用包装Execute在 try/except 块中。这是在ThreadProc函数中Classes。相关代码是:

try
  Thread.Execute;
except
  Thread.FFatalException := AcquireExceptionObject;
end;

事件OnTerminate处理程序在异常被捕获后被调用,因此您可以完全选择从那里重新显示它,尽管不是像我们上面发现的那样天真地提升它。

您的代码将如下所示:

procedure TMyThread.Execute;
begin
  raise Exception.Create('Thread Exception!!');
end;

procedure TMyThread.DoOnTerminate(Sender: TObject);
begin
  if Assigned(FatalException) and (FatalException is Exception) then
    QueueExceptionToMainThread(Exception(FatalException).Message);
end;

为了清楚起见QueueExceptionToMainThread,您必须编写一些功能!

于 2013-07-30T13:02:11.760 回答
4

AFAIKOnTerminate事件是用主线程调用的(Delphi 7源代码):

procedure TThread.DoTerminate;
begin
  if Assigned(FOnTerminate) then Synchronize(CallOnTerminate);
end;

Synchronize()方法实际上是在CheckSynchronize()上下文中执行的,在 Delphi 7 中,它会在远程线程中重新引发异常。

因此,引发异常OnTerminate是不安全的,或者至少没有任何用处,因为此时TMyThread.Execute已经超出范围。

简而言之,您的Execute方法中永远不会触发异常。

对于您的情况,我怀疑您不应该在 中引发任何异常OnTerminate而是设置一个全局变量(不是很漂亮),在线程安全的全局列表中添加一个项目(更好),和/或引发TEvent或发布 GDI 消息.

于 2013-07-30T11:32:59.280 回答
2

异常的同步调用不会阻止线程被中断。function ThreadProc后面的任何内容都Thread.DoTerminate;将被省略。

上面的代码有两个测试用例

  • 一个带有注释/未注释的同步异常 //**
  • 第二个在 OnTerminate 事件中带有(未)封装的异常,如果使用未封装,甚至会导致省略的破坏。

 

unit Unit1;

interface

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

type
  TMyThread=Class(TThread)
    private
      FExceptionClass: ExceptClass;
      FExceptionMessage: String;
    procedure DoOnTerminate(Sender: TObject);
    procedure SynChronizedException;
    procedure SynChronizedMessage;
    public
      procedure Execute;override;
      Destructor Destroy;override;
  End;
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private-Deklarationen }
  public
    { Public-Deklarationen }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}
procedure TMyThread.SynChronizedException;
begin
  Showmessage('> SynChronizedException');
  raise Exception.Create('Called Synchronized');
  Showmessage('< SynChronizedException'); // will never be seen
end;

procedure TMyThread.SynChronizedMessage;
begin
  Showmessage('After SynChronizedException');
end;

procedure TMyThread.Execute;
begin
  try
    OnTerminate :=  DoOnTerminate;      // first test
    Synchronize(SynChronizedException); //** comment this part for second test
    Synchronize(SynChronizedMessage); // will not be seen
    raise Exception.Create('Thread Exception!!');
  except
    on E:Exception do
    begin
      FExceptionClass := ExceptClass(E.ClassType);
      FExceptionMessage := E.Message;
    end;
  end;
end;

destructor TMyThread.Destroy;
begin
  Showmessage('Destroy ' + BoolToStr(Finished)) ;
  inherited;
end;

procedure TMyThread.DoOnTerminate(Sender: TObject);
begin
  {  with commented part above this will lead to a not called destructor
  if Assigned(FExceptionClass) then
      raise FExceptionClass.Create(FExceptionMessage);
  }
  if Assigned(FExceptionClass) then
      try // just silent for testing
        raise FExceptionClass.Create(FExceptionMessage);
      except
      end;

end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  With TMyThread.Create(false) do FreeOnTerminate := true;
  ShowMessage('Hallo');
end;

end.
于 2013-07-30T11:47:53.383 回答
0

我不知道你为什么要在主线程中引发异常,但我会假设它是做最少的异常处理 - 我认为这就像以一种很好的方式显示异常对象的 ClassName 和 Message在用户界面上。如果这就是您想要做的,那么如果您在线程中捕获异常,然后将 Exception.ClassName 和 Exception.Message 保存strings到主线程上的私有变量中。我知道这不是最先进的方法,但我已经这样做了,我知道它有效。一旦线程因异常而终止,您可以在 UI 上显示这两个字符串。您现在需要的只是一种通知主线程工作线程已终止的机制。我过去使用 Messages 实现了这一点,但我不记得具体细节了。

而不是试图解决“我如何通过做 B 来解决问题 A?” 您可以将您的情况重新定义为“我如何以任何可能的方式解决问题 A?”。

只是一个建议。希望对您的情况有所帮助。

于 2013-07-30T10:37:57.853 回答