在TThread Execute中引发异常?

9
我刚刚意识到我的线程中未能向用户显示异常!
起初,我在线程中使用了以下代码引发异常,但它并没有起作用:
except on E:Exception do
begin
  raise Exception.Create('Error: ' + E.Message);
end;

IDE向我显示异常,但我的应用程序没有!

我寻找解决方案,这是我找到的:

Delphi线程异常机制

http://www.experts-exchange.com/Programming/Languages/Pascal/Delphi/Q_22039681.html

我尝试了这两种方法,但都没有成功。

这是我的线程单元:

unit uCheckForUpdateThread;

interface

uses
  Windows, IdBaseComponent, IdComponent, IdTCPConnection, IdTCPClient,
  IdHTTP, GlobalFuncs, Classes, HtmlExtractor, SysUtils, Forms;

type
  TUpdaterThread = class(TThread)
  private
    FileGrabber : THtmlExtractor;
    HTTP : TIdHttp;
    AppMajor,
    AppMinor,
    AppRelease : Integer;
    UpdateText : string;
    VersionStr : string;
    ExceptionText : string;
    FException: Exception;
    procedure DoHandleException;
    procedure SyncUpdateLbl;
    procedure SyncFinalize;
  public
    constructor Create;

  protected
    procedure HandleException; virtual;

    procedure Execute; override;
  end;

implementation

uses
  uMain;

{ TUpdaterThread }

constructor TUpdaterThread.Create;
begin
  inherited Create(False);
end;

procedure TUpdaterThread.Execute;
begin
  inherited;
  FreeOnTerminate := True;

  if Terminated then
    Exit;

  FileGrabber           := THtmlExtractor.Create;
  HTTP                  := TIdHTTP.Create(nil);
  try
    try
      FileGrabber.Grab('http://jeffijoe.com/xSky/Updates/CheckForUpdates.php');
    except on E: Exception do
    begin
      UpdateText := 'Error while updating xSky!';
      ExceptionText := 'Error: Cannot find remote file! Please restart xSky and try again! Also, make sure you are connected to the Internet, and that your Firewall is not blocking xSky!';
      HandleException;
    end;
    end;

    try
      AppMajor      := StrToInt(FileGrabber.ExtractValue('AppMajor[', ']'));
      AppMinor      := StrToInt(FileGrabber.ExtractValue('AppMinor[', ']'));
      AppRelease    := StrToInt(FileGrabber.ExtractValue('AppRelease[[', ']'));
    except on E:Exception do
    begin
      HandleException;
    end;
    end;

    if (APP_VER_MAJOR < AppMajor) or (APP_VER_MINOR < AppMinor) or (APP_VER_RELEASE < AppRelease) then
    begin
      VersionStr := Format('%d.%d.%d', [AppMajor, AppMinor, AppRelease]);
      UpdateText := 'Downloading Version ' + VersionStr;
      Synchronize(SyncUpdateLbl);
    end;

  finally
    FileGrabber.Free;
    HTTP.Free;
  end;
  Synchronize(SyncFinalize);
end;

procedure TUpdaterThread.SyncFinalize;
begin
  DoTransition(frmMain.TransSearcher3, frmMain.gbLogin, True, 500);
end;

procedure TUpdaterThread.SyncUpdateLbl;
begin
  frmMain.lblCheckingForUpdates.Caption := UpdateText;
end;

procedure TUpdaterThread.HandleException;
begin
  FException := Exception(ExceptObject);
  try
    Synchronize(DoHandleException);
  finally
    FException := nil;
  end;
end;

procedure TUpdaterThread.DoHandleException;
begin
  Application.ShowException(FException);
end;

end.

如果您需要更多信息,请告诉我。
再次强调:IDE捕获所有异常,但我的程序不显示它们。
编辑:最终是Cosmin的解决方案起作用了 - 而之前没有起作用的原因是因为我没有添加ErrMsg变量,而是将变量包含的任何内容直接放入Synchronize中,这是行不通的,但我不知道为什么。当我没有其他想法时,我意识到了这一点,并且只是尝试各种解决方案。
像往常一样,我自己成了笑柄。= P

我从代码中删除了 Raise,因为它不能正常工作。我也尝试了使用同步的 raise,但它也不能正常工作 - 这就是 ExceptionText 出现的原因,我忘记将其删除了。 - Jeff
@Rafael - TIdHTTP控件引起的异常,因为我知道Web服务器上的文件不存在。我正在测试异常逻辑是否真正起作用,当我意识到它没有时,我感到震惊。 - Jeff
“不被引发”是指“没有向用户显示错误消息框”吗? - CodesInChaos
@Lasse - 只有第二个异常被触发了,尽管IDE也会引发第一个异常 - 也就是说,我在Execute过程中的HandleException方法上设置了断点。我的应用程序显示第二个异常,但不显示引发EIdHTTPProtocolException的第一个异常。 - Jeff
2
Jeff,如果你分不清“用户看不到异常”和“异常没有被抛出”,那么你也分不清“我整天都被困在室内”和“今天太阳没升起来”。仅仅因为你没有收到通知并不意味着事情没有发生。请编辑你的问题,更加精确地说明到底发生了什么或者没有发生什么,以及你的期望是什么。 - Rob Kennedy
显示剩余11条评论
6个回答

14

关于多线程开发,你需要理解的非常重要的一点是:

每个线程都有自己的调用堆栈,几乎就像它们是单独的程序一样。这包括你的程序的主线程。

线程只能以特定的方式相互交互:

  • 它们可以操作共享数据或对象。这会导致并发问题“竞态条件”,因此您需要能够帮助它们“优雅地共享数据”。这将引出下一个问题。
  • 它们可以使用各种操作系统支持例程来“发送信号”给彼此。其中包括:
    • 互斥锁
    • 临界区
    • 事件
  • 最后,您可以向其他线程发送消息。前提是该线程以某种方式被编写为消息接收器。

注意:请注意,线程不能在严格意义上直接调用其他线程。例如,如果线程A试图直接调用线程B,那么这将是线程A调用堆栈的一步!

这将我们带到了这个问题的话题:“我的线程中没有引发异常”

原因是所有异常所做的就是:

  • 记录错误
  • 回滚调用堆栈。<-- 注意: 您的TThread实例无法回滚主线程的调用堆栈,并且不能随意中断主线程的执行。

因此,TThread 不会自动向您的主应用程序报告异常。

您必须明确决定如何处理线程中的错误,并进行相应的实现。

解决方案

  • 第一步与单线程应用程序相同。您需要决定错误 表示什么 以及线程应该如何反应。
    • 线程是否应继续处理?
    • 线程是否应中止?
    • 错误是否需要被记录/报告?
    • 是否需要用户做出决策?<-- 这是最难实现的,因此我们现在跳过它。
  • 一旦这个决定被做出,实现适当的异常处理程序。
  • TIP: 确保异常不会逃逸出线程。如果发生这种情况,操作系统将不喜欢您。
  • 如果您需要主程序(线程)向用户报告错误,则有几个选项。
    • 如果线程被编写为返回结果对象,那么这很容易:进行更改,以便在出现错误时可以将该错误返回给该对象。
    • 向主线程发送消息以报告错误。请注意,主线程已经实现了一个消息循环,因此一旦处理该消息,应用程序将立即报告错误。

编辑:指定要求的代码示例。

如果您只想通知用户,则Cosmind Prund's answer对于Delphi 2010应该完美地工作。旧版本的Delphi需要更多的工作。以下是概念上类似于procedure TUpdaterThread.ShowException; begin MessageDlg(FExceptionMessage, mtError, [mbOk], 0); end; procedure TUpdaterThread.Execute; begin try raise Exception.Create('Test Exception'); //The code for your thread goes here // // except //Based on your requirement, the except block should be the outer-most block of your code on E: Exception do begin FExceptionMessage := 'Exception: '+E.ClassName+'. '+E.Message; Synchronize(ShowException); end; end; end;

对Jeff自己的答案进行了一些重要的更正,包括他问题中展示的实现:

调用Terminate仅在你的线程实现于while not Terminated do ... 循环内时才相关。看看Terminate方法实际上做了什么。

调用Exit是不必要的浪费,但你可能之所以这样做是因为接下来的错误。

在你的问题中,你将每个步骤都包装在自己的try...except中处理异常。这是一个绝对不可以的做法!通过这样做,你假装即使发生了异常,一切都很好。你的线程尝试下一步,但事实上肯定会失败!这不是处理异常的方式!


谢谢您提供的信息,我之前不知道关于调用堆栈的事实!我已经决定了当异常发生时我想让我的线程做什么 - 我希望它终止,并让用户知道出了什么问题,并给他们 Exception.Message。该线程不返回任何内容(我甚至不知道它可以这样做,所以我可以像函数一样使用它吗?)。 - Jeff
Jeff,线程执行“停止”的唯一时刻是线程终止。到达“except”块并不会导致终止。调试器可以“中断”线程执行,但这与终止不同,而且被调试的应用程序对调试器对其的操作没有直接的了解。您还可以通过睡眠、等待或向另一个线程发送消息来“暂停”执行。这些也不同于终止。 - Rob Kennedy
@Jeff:某种程度上说,您的线程已经返回结果了。它正在更新主窗体上的标签。至于在异常情况下停止执行:如果您在while not Terminated循环内处理异常,则会继续执行。但是,如果您不处理异常,则会将调用堆栈展开回线程的入口点并终止。然而,大多数操作系统面对这种“不良行为”时都会导致应用程序崩溃。 - Disillusioned

9
这是关于该问题的简短观点。它仅适用于Delphi 2010+(因为该版本引入了匿名方法)。与已经发布的更复杂的方法不同,我的方法只显示错误消息,没有其他内容。
procedure TErrThread.Execute;
var ErrMsg: string;
begin
  try
    raise Exception.Create('Demonstration purposes exception');
  except on E:Exception do
    begin
      ErrMsg := E.ClassName + ' with message ' + E.Message;
      // The following could be all written on a single line to be more copy-paste friendly  
      Synchronize(
        procedure
        begin
          ShowMessage(ErrMsg);
        end
      );
    end;
  end;
end;

就像其他情况一样,只有IDE显示错误,应用程序不会引发异常 :( - Jeff
@David - 如果我在线程外执行,那么就不需要同步或任何与TThread相关的东西。 - Jeff
@jeff 很难说。一定是我们看不到的东西。 - David Heffernan
1
@Jeff,将我的代码原样复制粘贴到新的表单应用程序中,告诉我们它是否显示错误。那段代码是有效的,我正在生产中使用它,并在发布之前也进行了测试。而且它只在Synchronize中执行ShowMessage,我真的很难相信它不起作用。 - Cosmin Prund
@Cosmin - 新项目运行良好。我将编辑我的原始帖子,向您展示代码在哪里无法正常工作(并且仍然无法正常工作)。 - Jeff
显示剩余6条评论

6

线程不会自动将异常传播到其他线程,因此您必须自己处理它。

Rafael已经概述了一种方法,但还有其他选择。 Rafael指向的解决方案通过将异常调度到主线程中同步处理。

在我自己使用线程池的一个例子中,线程捕获并接管了异常的所有权。 这使得控制线程可以根据自己的意愿进行处理。

代码如下。

procedure TMyThread.Execute;
begin
  Try
    DoStuff;
  Except
    on Exception do begin
      FExceptAddr := ExceptAddr;
      FException := AcquireExceptionObject;
      //FBugReport := GetBugReportCallStackEtcFromMadExceptOrSimilar.
    end;
  End;
end;

如果控制线程决定触发异常,可以像这样执行:

raise Thread.FException at Thread.FExceptAddr;

有时您可能有无法调用Synchronize的代码,例如某些DLL,这种方法非常有用。
请注意,如果您不引发已捕获的异常,则需要销毁它,否则会导致内存泄漏。

@David - 那我应该说什么呢?我会发布我所拥有的信息。 - Jeff
@Jeff:请记住,我们不是心理学家或者预言家,我们不知道你的代码库,也无法看到你屏幕上的内容,我们不知道你采取了哪些步骤、期望得到什么结果以及实际获得了什么结果。如果你想得到好的答案,你需要为我们提供所有这些信息,以便我们有机会帮助你。否则我们只能猜测问题可能出在哪里以及你可能如何解决它... - Marjan Venema
1
David,恭喜你获得了黄金 Delphi 徽章。另外,因为我学到了新东西,所以给你点赞 AcquireExceptionObject - Cosmin Prund
@jeff 请阅读此答案的第一条评论,并设身处地地考虑我们的情况。无论如何,听起来“同步”可能是您需要的。 - David Heffernan
@David - 是的,我明白你的意思,但故障是相同的:只有IDE告诉我有异常。而且,使用Synchronize(如答案建议的那样)在我的应用程序中也不会引发异常。我不知道为什么会这样..我想能够显示自定义消息,然后是实际的Exception.Message,就像在常规的TryExcept块中所做的那样。然而,Synchronize应该可以胜任,不是吗? 我尝试使用Synchronize引发异常,因此,由于它在主线程中执行,所以它应该有效? - Jeff
显示剩余2条评论

4

好的,

没有您的源代码会很难,但我已经测试过这个:

如何在TThread对象中处理异常

它很好用。也许你应该看一下它。

编辑:

你没有按照你提出的链接所告诉我们要做的去做。检查我的链接,你会看到如何做。

编辑2:

尝试一下这个,然后告诉我它是否起作用:

 TUpdaterThread= class(TThread)
 private
   FException: Exception;
   procedure DoHandleException;
 protected
   procedure Execute; override;
   procedure HandleException; virtual;
 end;

procedure TUpdaterThread.Execute;
begin
  inherited;
  FreeOnTerminate := True;
  if Terminated then
    Exit;
  FileGrabber := THtmlExtractor.Create;
  HTTP := TIdHTTP.Create(Nil);
  try
    Try
      FileGrabber.Grab('http://jeffijoe.com/xSky/Updates/CheckForUpdates.php');
    Except
      HandleException;
    End;
    Try
      AppMajor := StrToInt(FileGrabber.ExtractValue('AppMajor[', ']'));
      AppMinor := StrToInt(FileGrabber.ExtractValue('AppMinor[', ']'));
      AppRelease := StrToInt(FileGrabber.ExtractValue('AppRelease[[', ']'));
    Except
      HandleException;
    End;
    if (APP_VER_MAJOR < AppMajor) or (APP_VER_MINOR < AppMinor) or (APP_VER_RELEASE < AppRelease) then begin
      VersionStr := Format('%d.%d.%d', [AppMajor, AppMinor, AppRelease]);
      UpdateText := 'Downloading Version ' + VersionStr;
      Synchronize(SyncUpdateLbl);
    end;
  finally
    FileGrabber.Free;
    HTTP.Free;
  end;
  Synchronize(SyncFinalize);

end;

procedure TUpdaterThread.HandleException;
begin
  FException := Exception(ExceptObject);
  try
    Synchronize(DoHandleException);
  finally
    FException := nil;
  end;
end;

procedure TMyThread.DoHandleException;
begin
  Application.ShowException(FException);
end;

编辑3:

你说你无法捕获EIdHTTPProtocolException。但是它对我有效。尝试这个示例,亲眼看看:

procedure TUpdaterThread.Execute;
begin
  Try
    raise EIdHTTPProtocolException.Create('test');
  Except
    HandleException;
  End;
end;

@Rafael - 我移除了 Raise 的部分,因为它没有起作用,我正在尝试其他方法等。相信我,我已经尝试过这些方法了。 - Jeff
好的,那我真的不知道,因为我在这里尝试了这段代码,它可以正常工作。 - Rafael Colucci
它确实处理EIdHTTPProtocolException。我已经更改了源代码以引发EIdHTTPProtocolException,它也可以正常工作。程序不会停止执行,因为你正在使用线程。如果你想让程序在线程异常时停止,你应该向主线程应用程序发出线程错误信号,并在主线程检查此信号以中止程序执行。 - Rafael Colucci
“我该如何将自定义消息分配给它?”你只需要像这样重新引发异常:raise Exception.Create('My error: ' + E.message) - Rafael Colucci
@Rafael - 如果我在 DoHandleException 中重新引发异常,它不会在 IDE 外部引发任何异常。 - Jeff
显示剩余6条评论

2

我之前使用了TWMCopyData和SendMessage进行线程间通信,因此我认为以下方法应该适用:

Const MyAppThreadError = WM_APP + 1;

constructor TUpdaterThread.Create(ErrorRecieverHandle: THandle);
begin
    Inherited Create(False);
    FErrorRecieverHandle := Application.Handle;
end;

procedure TUpdaterThread.Execute;
var
    cds: TWMCopyData;
begin
  try
     DoStuff;
  except on E:Exception do
    begin
        cds.dwData := 0;
        cds.cbData := Length(E.message) * SizeOf(Char);
        cds.lpData := Pointer(@E.message[1]);         
        SendMessage(FErrorRecieverHandle, MyAppThreadError, LPARAM(@cds), 0);
    end;
  end;
end;

我只用它来发送简单的数据类型或字符串,但我相信它可以适应根据需要发送更多信息。

您需要在创建线程的表单中的构造函数中添加Self.Handle并在创建它的表单中处理消息。

procedure HandleUpdateError(var Message:TMessage); message MyAppThreadError;
var
    StringValue: string;
    CopyData : TWMCopyData; 
begin
    CopyData := TWMCopyData(Msg);
    SetLength(StringValue, CopyData.CopyDataStruct.cbData div SizeOf(Char));
    Move(CopyData.CopyDataStruct.lpData^, StringValue[1], CopyData.CopyDataStruct.cbData);
    Message.Result := 0;
    ShowMessage(StringValue);
end;

1

奇怪的是,每个人都回答了这个问题,但却没有发现明显的问题:由于在后台线程中引发的异常是异步的,并且可以在任何时候发生,这意味着从后台线程显示异常将在随机时间弹出对话框给用户,很可能显示与用户当前操作无关的异常。我怀疑这样做可能不会增强用户体验。


谁说它与用户正在做的事情无关?并不是所有在线程中运行的东西都是如此。 - David Heffernan
真的吗?但是考虑到基于线程的异步工作的性质,你怎么知道呢?在这里,过程的一部分肯定不仅是盲目地回答用户的问题,而且有时要建议用户提出错误的问题。在这种情况下,也许在表单/状态栏中显示错误才是真正需要的,而弹出由异步线程处理引发的对话框呢?有人真的想这样对待用户吗? - Misha
@Misha 所有的观点都很好,但是Jeff的这个问题是一个长期运行的系列问题的一部分。据我所知,他正在使用线程响应用户操作,通过一些Skype API进行通信。他使用线程是因为如果他在主线程中执行,则他的UI将变得无响应。因此,您完全正确地质疑显示对话框的智慧,但我认为在这种情况下,Jeff可能做得很对。 - David Heffernan
1
我们没有向他解释那个,因为那不是他问的问题。他需要帮助他的线程,而不是解释如何向用户显示错误。 - Rafael Colucci
虽然与问题无关,但这是关于错误主题的优秀通用评论。即使在单线程环境中,错误对用户体验也会产生相当大的干扰 - 而且用户通常不会阅读它们。 (只需看看有多少个SO问题陈述:我遇到了一个错误,而没有给出任何提示信息。) 在某些情况下,将错误添加到“消息窗口”或动态显示指示已发生错误的按钮/图标可能更实用。 - Disillusioned

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