Can I raise an exception from within OnTerminate event of a TThread?
Asked Answered
L

4

5

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

Letitialetizia answered 30/7, 2013 at 7:24 Comment(9)
From my experiments, raising an exception in an OnTerminate event handler results in Halt being called. I think you'll need to find a different way to re-surface the exception.Odilia
Some useful ideas here #2923730Infiltrate
@David Heffernan: Halt? I never found this situation. Can you explain please?Letitialetizia
No I cannot explain. But my reproduction of your scenario led to Halt and then process termination.Odilia
Oh, that's interesting. Thank you.Letitialetizia
I edited my question because now I see that my new exception is raised at a different level in the call stack with respect to who started the thread. Could it be this?Letitialetizia
I think it might be nice to have a complete program to work with. Could you make an SSCCE using a console app? Make it as short as possible and yet still exhibiting the behaviour.Odilia
Sure it would be nice, if only I had enough time to pack an SSCCE now... :-) I'll try to distill something as soon as I understand exactly where the problem is. Thank you very much for your time!Letitialetizia
@Letitialetizia I've got your explanation now.Odilia
O
6

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!

Odilia answered 30/7, 2013 at 13:2 Comment(5)
Yes, that makes sense. There's a good reason for Synchronize trapping exceptions and then raising them back on the calling thread. If they were to be raised on the main thread, then the exceptions would interrupt the execution of code on the main thread. You can re-raise them on the main thread if you get the main thread to co-operate. You can let the main thread re-raise them at a well-defined point. You need the main thread to opt-in to this mechanism. By the way, when you accepted Arnaud's answer, it contained a number of errors. It is better now after his edits, prompted by my comments.Odilia
Ok, now I got it: Exceptions within Execute are handled differently than those within OnTerminate. So I can override the virtual DoTerminate and call a (Synchronized) custom method that wraps the call to OnTerminate in a try..except block. This handler will save the Exception class name and message in two fields the same way my Execute exception handler does. This way I can safely handle exceptions wherever they arise. I will not be able to re-raise them in the main thread, but I could notify it of the abnormal termination and pass it the exception message or so.Letitialetizia
Er, my comment above was in response to your original comment, the one that you just re-posted!Odilia
Sorry I rewrote my comment when you were answering. But the sense doesn't change. My class is the base class of several other specialized threads: Some of them, for example, launch external processes and wait for them to terminate. In this case (if an exception aries) I can use the saved class/message to raise an identical exception within the main thread (not in OnTerminate but after the call to the thread), just because it was a blocking call. As for voting and acceptance, it's very difficult for me to accept just one answer when many of you gave me so many useful hints :-)Letitialetizia
I don't care which answer you accept, I just want to be sure that you understand that the answer that you accepted contained many errors at the time that you accepted it. I want to make sure that you don't think that what Arnaud originally said was accurate. I never like it when an answer is accepted that contains errors.Odilia
L
4

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.

Lonna answered 30/7, 2013 at 11:32 Comment(3)
-1 Your facts are not correct. In fact Synchronize is protected against exceptions, by an exception handler. Exceptions raised are caught and re-raised on the thread that called Synchronize. The problem is that re-raise happens near the death of the thread and at a point where there is no exception handler. My answer gives the details.Odilia
@DavidHeffernan You are right: I missed a try..except in CheckSynchronize(). It stores the exception in SyncProc.SyncRec.FSynchronizeException which raise it in the calling thread. But my conclusion is still valid: do not raise exception in OnTerminate events, since the thread is terminated, but notify the main thread by another mean.Lonna
@ArnaudBouchez Indeed. That's exactly what my answer explains, in some detail.Odilia
Q
2

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.
Quadruped answered 30/7, 2013 at 11:47 Comment(0)
I
0

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.

Infiltrate answered 30/7, 2013 at 10:37 Comment(4)
A suggestion like this is better as a comment. I expect that it got downvoted because it did not answer the question that was asked.Odilia
@DavidHeffernan No worries. Nice to see both your and Arnaud's answers were effectively implying the same thing as my suggestion, just varying in implementation and sophistication.Infiltrate
The point is that you are suggesting is exactly what is attempted in the question. So, why doesn't it work? That's the question.Odilia
@DavidHeffernan I thought there might be a more important question behind the literal wording of the question. That is "how to achieve the desire of handling thread exceptions in the main thread". I think that makes my answer a valid candidate for Arnaud's "notify the main thread by another mean", a comment to which you concurred and provided your own implementation in your answer. It may be a low quality answer, I grant you that, but I think it's a valid answer, and at least a reasonable suggestion. I honestly don't mind the downvote, I enjoy the human interaction. Have a laugh! Cheers.Infiltrate

© 2022 - 2024 — McMap. All rights reserved.