Question

I wrote a TThread descendant class that, if an exception is raised, saves exception's Class and Message in two private fields

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

I thought I could raise a similar exception in the OnTerminate event, so that the main thread could handle it (here is a simplified version):

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;

I expect that the standard exception handling mechanism occurs (an error dialog box), but I get mixed results: The dialog appears but is followed by a system error, or or (more funny) the dialog appears but the function that called the thread goes on as if the exception were never raised.
I guess that the problem is about the call stack.
Is it a bad idea?
Is there another way to decouple thread exceptions from the main thread but reproducing them the standard way?
Thank you

Was it helpful?

Solution

The fundamental issue in this question, to my mind is:

What happens when you raise an exception in a thread's OnTerminate event handler.

A thread's OnTerminate event handler is invoked on the main thread, by a call to Synchronize. Now, your OnTerminate event handler is raising an exception. So we need to work out how that exception propagates.

If you examine the call stack in your OnTerminate event handler you will see that it is called on the main thread from CheckSynchronize. The code that is relevant is this:

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

So, CheckSynchronize catches your exception and stashes it away in FSynchronizeException. Excecution then continues, and FSynchronizeException is later raised. And it turns out, that the stashed away exception is raised in TThread.Synchronize. The last dying act of TThread.Synchronize is:

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

What this means is that your attempts to get the exception to be raised in the main thread have been thwarted by the framework which moved it back onto your thread. Now, this is something of a disaster because at the point at which raise ASyncRec.FSynchronizeException is executed, in this scenario, there is no exception handler active. That means that the thread procedure will throw an SEH exception. And that will bring the house down.

So, my conclusion from all this is the following rule:

      Never raise an exception in a thread's OnTerminate event handler.

You will have to find a different way to surface this event in your main thread. For example, queueing a message to the main thread, for example by a call to PostMessage.


As an aside, you don't need to implement an exception handler in your Execute method since TThread already does so.

The implementation of TThread wraps the call to Execute in an try/except block. This is in the ThreadProc function in Classes. The pertinent code is:

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

The OnTerminate event handler is called after the exception has been caught and so you could perfectly well elect to re-surface it from there, although not by naively raising it as we discovered above.

Your code would then look like this:

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;

And just to be clear, QueueExceptionToMainThread is some functionality that you have to write!

OTHER TIPS

AFAIK the OnTerminate event is called with the main thread (Delphi 7 source code):

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

The Synchronize() method is in fact executed in the CheckSynchronize() context, and in Delphi 7, it will re-raise the exception in the remote thread.

Therefore, raising an exception in OnTerminate is unsafe or at least without any usefulness, since at this time TMyThread.Execute is already out of scope.

In short, the exception will never be triggered in your Execute method.

For your case, I suspect you should not raise any exception in OnTerminate, but rather set a global variable (not very beautiful), add an item in a thread-safe global list (better), and/or raise a TEvent or post a GDI message.

A synchonized call of an exception will not prevent the thread from being interrupted. Anything in function ThreadProc after Thread.DoTerminate; will be omitted.

The code above has two test cases

  • One with commented / uncommented synchronized exception //**
  • The second with (un)encapsulated exception in the OnTerminate event, which will lead even to an omitted destruction if used unencapsulated.

 

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.

I don't know why you want to raise the exception in the main thread, but I will assume it is to do minimal exception handling - which I would consider to be something like displaying the ClassName and Message of the Exception object in a nice way on the UI. If this is all you want to do then how about if you catch the exception in your thread, then save the Exception.ClassName and Exception.Message strings to private variables on the main thread. I know it's not the most advanced method, but I've done this and I know it works. Once the thread terminates because of the exception you can display those 2 strings on the UI. All you need now is a mechanism for notifying the main thread that the worker thread has terminated. I've achieved this in the past using Messages but I can't remember the specifics.

Rather than try to solve "How do I solve problem A by doing B?" you could reframe your situation as "How do I solve problem A whichever way possible?".

Just a suggestion. Hope it helps your situation.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top