Delphi thread exception mechanism
Asked Answered
L

4

17

I have a dilemma on how threads work in delphi, and why at a moment when a thread should raise an exception, the exception is not showed. bellow is the code with comments, maybe somebody cand explain to me how that thread, or delphi, is managing access violations

//thread code

unit Unit2;

interface

uses
  Classes,
  Dialogs,
  SysUtils,
  StdCtrls;

type
  TTest = class(TThread)
  private
  protected
    j: Integer;
    procedure Execute; override;
    procedure setNr;
  public
    aBtn: tbutton;
  end;

implementation


{ TTest }

procedure TTest.Execute;
var
  i                 : Integer;
  a                 : TStringList;
begin
 // make severals operations only for having something to do
  j := 0;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;

  Synchronize(setnr);
  a[2] := 'dbwdbkbckbk'; //this should raise an AV!!!!!!

end;

procedure TTest.setNr;
begin
  aBtn.Caption := IntToStr(j)
end;

end.

project's code

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs,
  Unit2, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
  public
    nrthd:Integer;
    acrit:TRTLCriticalSection;
    procedure bla();
    procedure bla1();
    function bla2():boolean;
    procedure onterm(Sender:TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.bla;
begin
 try
  bla1;
 except on e:Exception do
   ShowMessage('bla '+e.Message);
 end;
end;

procedure TForm1.bla1;
begin
 try
  bla2
 except on e:Exception do
   ShowMessage('bla1 '+e.Message);
 end;
end;

function TForm1.bla2: boolean;
var ath:TTest;
begin
 try
  ath:=TTest.Create(true);
   InterlockedIncrement(nrthd);
  ath.FreeOnTerminate:=True;
  ath.aBtn:=Button1;
  ath.OnTerminate:=onterm; 
   ath.Resume;
 except on e:Exception do
  ShowMessage('bla2 '+e.Message);
 end;
end;

procedure TForm1.Button1Click(Sender: TObject);

begin
//
 try
   bla;
   while nrthd>0 do
    Application.ProcessMessages;
 except on e:Exception do
  ShowMessage('Button1Click '+e.Message);
 end;
 ShowMessage('done with this');
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
 nrthd:=0;
end;

procedure TForm1.onterm(Sender: TObject);
begin
 InterlockedDecrement(nrthd)
end;

end.

the purpose of this application is only to know where the access violation is catched, and how the code should be written.
I can not understand why in the line "a[2] := 'dbwdbkbckbk';" the AV is not raised.

Lafferty answered 2/9, 2010 at 13:51 Comment(3)
Didn't the debugger tell you about the exception?Landed
The "a" variable is not initialized! What if it points to a VALID memory address? I mean an address that the process owns. Then your code will write at that location without an Access Violation. Am I right? I think you should at least put a to NIL.Mons
See Remy Lebeau's comment here. It explains what an AV is and how it occurs: https://mcmap.net/q/161745/-what-does-access-violation-meanMons
L
23

In Delphi 2005 — and probably most other versions — if an exception escapes from the Execute method without being handled, then it is caught by the function that called Execute and stored in the thread's FatalException property. (Look in Classes.pas, ThreadProc.) Nothing further is done with that exception until the thread is freed, at which point the exception is also freed.

It's your responsibility, therefore, to check that property and do something about it. You can check it in the thread's OnTerminate handler. If it's non-null, then the thread terminated due to an uncaught exception. So, for example:

procedure TForm1.onterm(Sender: TObject);
var
  ex: TObject;
begin
  Assert(Sender is TThread);
  ex := TThread(Sender).FatalException;
  if Assigned(ex) then begin
    // Thread terminated due to an exception
    if ex is Exception then
      Application.ShowException(Exception(ex))
    else
      ShowMessage(ex.ClassName);
  end else begin
    // Thread terminated cleanly
  end;
  Dec(nrthd);
end;

There's no need for the interlocked functions for tracking your thread count. Both your thread-creation function and your termination handler always run in the context of the main thread. Plain old Inc and Dec are sufficient.

Landed answered 2/9, 2010 at 14:14 Comment(6)
+1. I've never seen FatalException?... oh wait, we're still on Delphi 5.Soileau
I don't have the source code for that version anymore, @Lieven. If it has AcquireExceptionObject, then you can mimic the new FatalException behavior yourself.Landed
@Lieven: In D5, and D6 as well I think, the thread's execute method wasn't protected yet... You had to do it yourself within an Execute's override.Brachium
The TThread.FatalException property (and the System.AcquireExceptionObject() function) was introduced in D6. In D5, Execute() is wrapped only by a try..finally to ensure DoTerminate() is always called, but the thread is terminated (via EndThread()) before a raised exception is dispatched to handlers beyond the finally. In D6, TThread.Execute() is wrapped by a try..except that acquires the exception and stores it in FatalException before calling DoTerminate().Jone
Works on Delphi 10.1 BerlinTallou
Thanks @alitrun, there are little deferences since d5 that stack to each others through versions, every piece of information has to be version-specified... it's hard to know when an "unversionned" solution should work.Courtier
S
13

Threading is one place where you should swallow exceptions.

The gist of handling Exceptions in threads is that if you want the exception to be shown to the end user, you should capture it and pass it on to the main thread where it can safely be shown.

You'll find some examples in this EDN thread How to Handle exceptions in TThread Objects.

procedure TMyThread.DoHandleException;
begin
  // Cancel the mouse capture
  if GetCapture <> 0 then SendMessage(GetCapture, WM_CANCELMODE, 0, 0);
  // Now actually show the exception
  if FException is Exception then
    Application.ShowException(FException)
  else
    SysUtils.ShowException(FException, nil);
end;

procedure TMyThread.Execute;
begin
  FException := nil;
  try
    // raise an Exception
    raise Exception.Create('I raised an exception');
  except
    HandleException;
  end;
end;

procedure TMyThread.HandleException;
begin
  // This function is virtual so you can override it
  // and add your own functionality.
  FException := Exception(ExceptObject);
  try
    // Don't show EAbort messages
    if not (FException is EAbort) then
      Synchronize(DoHandleException);
  finally
    FException := nil;
  end;
end;
Soileau answered 2/9, 2010 at 14:3 Comment(0)
Z
0

We can also reraise FatalException. Reraising seems not logical but if you have an central exception/error handler in your code and and if you just want to include thread exceptions into that mechanisim, you can reraise on some rare situation :

procedure TForm1.onterm(Sender: TObject);
var
  ex: Exception;
begin
  Assert(Sender is TThread);
  ex := Exception(TThread(Sender).FatalException);
  if Assigned(ex) then
    // Thread terminated due to an exception
    raise ex;
  Dec(nrthd);
end;
Zenas answered 30/3, 2011 at 16:1 Comment(3)
Are you sure this works? What about freeing that exception object?Econah
This doesn't work (at least in XE2). I tried it and got a non-modal message dialog followed by a EOSError.Logy
The OnTerminate handler is called by Synchronize() inside of the worker thread context after Execute() exits. If an exception is raised inside of Synchronize(), it acquires the exception and re-raises it in the worker thread context. So raising an exception inside of OnTerminate is a bad thing. To do this correctly, you would have to manually take ownership of the FatalException, somehow reset TThread.FFatalException to nil (so TThread does not destroy the object), and re-raise it in the main thread yourself after Synchronize() has exited. Hacks are required to take ownershipJone
M
0

Maybe the example you show is not the best because the "a" variable is not initialized! It can point to ANY possible memory location in your computer. It can even point to locations that don't exist physically (even though this is moot due to the virtual memory system).

So, in your program, if "a" points by accident to a VALID memory address (I mean an address that the process owns) then your code will write to that location without an Access Violation. I think you should at least put "a" to NIL. After that, take a look at Lieven Keersmaekers post.

See Remy Lebeau's comment here: https://mcmap.net/q/161745/-what-does-access-violation-mean
And this also: Why uninitialized pointers cause mem access violations close to 0?

Mons answered 12/2, 2019 at 8:36 Comment(2)
The cause of the problem is well known. In fact, the problem with a was included in this question intentionally to demonstrate triggering an exception in the Execute method. No guidance was necessary as to the cause of the exception. The question asked why the exception wasn't reported like other exceptions typically are. Assigning nil to a wouldn't have changed anything, either.Landed
"to demonstrate triggering an exception" - as long as a[2] points to a random memory address, the exception is not guaranteed. In rare situations, the program would be able to write at a[2].Mons

© 2022 - 2024 — McMap. All rights reserved.