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.

RBA
  • 12,337
  • 16
  • 79
  • 126
  • 1
    Didn't the debugger tell you about the exception? – Rob Kennedy Sep 02 '10 at 14:15
  • 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. – Gabriel Feb 12 '19 at 08:27
  • See Remy Lebeau's comment here. It explains what an AV is and how it occurs: https://stackoverflow.com/a/16071764/46207 – Gabriel Feb 12 '19 at 08:31

4 Answers4

22

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.

Rob Kennedy
  • 161,384
  • 21
  • 275
  • 467
  • +1. I've never seen FatalException?... oh wait, we're still on Delphi 5. – Lieven Keersmaekers Sep 02 '10 at 14:21
  • 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. – Rob Kennedy Sep 02 '10 at 14:43
  • @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. – Marjan Venema Sep 02 '10 at 18:22
  • 1
    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()`. – Remy Lebeau Aug 16 '16 at 02:05
  • 1
    Works on Delphi 10.1 Berlin – alitrun Jun 11 '17 at 03:09
  • 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. – Darkendorf Jan 03 '18 at 08:10
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;
Lieven Keersmaekers
  • 57,207
  • 13
  • 112
  • 146
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: http://www.stackoverflow.com/a/16071764/46207
And this also: Why uninitialized pointers cause mem access violations close to 0?

Gabriel
  • 20,797
  • 27
  • 159
  • 293
  • 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. – Rob Kennedy Jun 27 '22 at 21:47
  • "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]. – Gabriel Jun 29 '22 at 08:27
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;
RBA
  • 12,337
  • 16
  • 79
  • 126
  • 1
    Are you sure this works? What about freeing that exception object? – dummzeuch Aug 18 '11 at 08:11
  • 2
    This doesn't work (at least in XE2). I tried it and got a non-modal message dialog followed by a EOSError. – Jens Mühlenhoff Aug 19 '13 at 12:41
  • 1
    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 ownership – Remy Lebeau Aug 16 '16 at 02:19