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
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!
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.