【问题标题】:Can I raise an exception from within OnTerminate event of a TThread?我可以从 TThread 的 OnTerminate 事件中引发异常吗?
【发布时间】:2013-07-30 22:13:56
【问题描述】:

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

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

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

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;

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

【问题讨论】:

  • 根据我的实验,在OnTerminate 事件处理程序中引发异常会导致调用Halt。我认为您需要找到一种不同的方法来重新显示异常。
  • 这里有一些有用的想法stackoverflow.com/questions/2923230/…
  • @大卫赫弗南:Halt?我从来没有发现过这种情况。你能解释一下吗?
  • 不,我无法解释。但是我对您的场景的复制导致停止,然后进程终止。
  • 哦,这很有趣。谢谢。

标签: delphi delphi-7 tthread


【解决方案1】:

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

当您在线程的 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 块中。这是在Classes 中的ThreadProc 函数中。相关代码为:

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 是您必须编写的一些功能!

【讨论】:

  • 是的,这是有道理的。 Synchronize 捕获异常然后在调用线程上将它们提升回来是有充分理由的。如果要在主线程上引发它们,那么异常将中断主线程上代码的执行。如果您让主线程合作,您可以在主线程上重新提升它们。您可以让主线程在明确定义的点重新提升它们。您需要主线程选择加入此机制。顺便说一句,当您接受 Arnaud 的回答时,它包含许多错误。在我的 cmets 提示下,经过他的编辑,现在好多了。
  • 好的,现在我明白了:Execute 中的异常与OnTerminate 中的异常处理方式不同。所以我可以覆盖虚拟的DoTerminate 并调用一个(同步的)自定义方法,它将对OnTerminate 的调用包装在try..except 块中。这个处理程序将把异常类名和消息保存在两个字段中,就像我的Execute 异常处理程序一样。这样,无论何时出现异常,我都可以安全地处理它们。我将无法在主线程中重新引发它们,但我可以通知它异常终止并将异常消息传递给它左右。
  • 呃,我上面的评论是对您原来的评论的回应,您刚刚重新发布的评论!
  • 对不起,当你回答时我重写了我的评论。但感觉并没有改变。我的类是其他几个专用线程的基类:例如,它们中的一些启动外部进程并等待它们终止。在这种情况下(如果出现异常),我可以使用保存的类/消息在主线程中引发相同的异常(不是在 OnTerminate 中,而是在调用线程之后),只是因为它是一个阻塞调用。至于投票和接受,当你们中的许多人给了我这么多有用的提示时,我很难只接受一个答案:-)
  • 我不在乎你接受哪个答案,我只是想确保你明白你接受的答案在你接受它时包含许多错误。我想确保您不认为 Arnaud 最初所说的话是准确的。当接受包含错误的答案时,我从不喜欢它。
【解决方案2】:

AFAIK 使用主线程调用OnTerminate 事件(Delphi 7 源代码):

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

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

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

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

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

【讨论】:

  • -1 你的事实不正确。事实上,Synchronize 受到异常处理程序的保护,不受异常影响。在调用Synchronize 的线程上捕获并重新引发引发的异常。问题是重新引发发生在线程死亡附近并且没有异常处理程序的地方。我的回答给出了细节。
  • @DavidHeffernan 你是对的:我错过了一次尝试..除了 CheckSynchronize()。它将异常存储在 SyncProc.SyncRec.FSynchronizeException 中,该异常在调用线程中引发。但我的结论仍然有效:不要在 OnTerminate 事件中引发异常,因为线程已终止,而是通过另一种方式通知主线程。
  • @ArnaudBouchez 确实如此。这正是我的回答详细解释的内容。
【解决方案3】:

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

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

  • 一个带有注释/未注释的同步异常 //**
  • 第二个在 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.

【讨论】:

    【解决方案4】:

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

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

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

    【讨论】:

    • 这样的建议最好作为评论。我预计它会被否决,因为它没有回答所提出的问题。
    • @DavidHeffernan 不用担心。很高兴看到您和 Arnaud 的回答实际上暗示了与我的建议相同的内容,只是在实施和复杂性方面有所不同。
    • 关键是您所建议的正是问题中所尝试的。那么,为什么它不起作用?就是这个问题。
    • @DavidHeffernan 我认为问题的字面措辞背后可能有一个更重要的问题。那就是“如何实现在主线程中处理线程异常的愿望”。我认为这使我的回答成为 Arnaud 的“以另一种方式通知主线程”的有效候选者,这是您同意的评论并在您的回答中提供了您自己的实现。我承认这可能是一个低质量的答案,但我认为这是一个有效的答案,至少是一个合理的建议。老实说,我不介意投反对票,我喜欢人与人之间的互动。笑一下!干杯。
    猜你喜欢
    • 2014-07-29
    • 1970-01-01
    • 2015-12-30
    • 1970-01-01
    • 1970-01-01
    • 2012-10-31
    • 1970-01-01
    • 2023-03-14
    • 1970-01-01
    相关资源
    最近更新 更多