0

I've been trying to to set the FreeOnTerminate property in the OnTerminate procedure but it seems like it's either too late to set it or it's completely ignoring the write procedure.

How can I set/change the FreeOnTerminate property in the OnTerminate procedure? Are there any workarounds for that?

A little code:

unit Unit2;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm2 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    procedure OnTestThreadTerminate (Sender : TObject);
  public
    { Public declarations }
  end;

type
  TTestThread = class (TThread)
  public
    procedure Execute; override;
end;


var
  Form2: TForm2;
  GlobalThreadTest : TTestThread;

implementation

{$R *.dfm}

procedure TForm2.Button1Click(Sender: TObject);
begin
  GlobalThreadTest                    := TTestThread.Create (True);
  GlobalThreadTest.OnTerminate        := Self.OnTestThreadTerminate;
  GlobalThreadTest.FreeOnTerminate    := True;
  GlobalThreadTest.Resume;
end;

procedure TForm2.Button2Click(Sender: TObject);
begin
  // 2nd Button to try to free the thread...
  // AFTER BUTTON1 has been clicked!
  try
    GlobalThreadTest.Free;
  except
    on e : exception do begin
      MessageBox(Self.Handle, pchar(e.Message), pchar(e.ClassName), 64);
    end;
  end;
end;

procedure TForm2.OnTestThreadTerminate(Sender: TObject);
begin
  TTestThread(Sender).FreeOnTerminate := False;                        // Avoid freeing...
  ShowMessage (BoolToStr (TTestThread(Sender).FreeOnTerminate, True)); // FreeOnTerminate Value has been changed successfully!
end;

procedure TTestThread.Execute;
begin
  // No code needed for test purposes.
end;

end.
Ben
  • 3,380
  • 2
  • 44
  • 98
  • 1
    You have a data race. Once you set FreeOnTerminate to True, and call Resume or Start, then you are no longer able to refer to the thread reliably. The design of FreeOnTerminate is that it is set to True once and never subsequently modified. Whatever you are trying to do, your solution is wrong. – David Heffernan Nov 25 '13 at 08:20
  • 1
    Set FreeOnTerminate when you wish to absolve yourself of resposnsibility to call Free on the thread object. When you do so you must not hold a reference to the thread instance. If you want some involvement in destruction of the thread then do not set FreeOnTerminate. – David Heffernan Nov 25 '13 at 08:21
  • @DavidHeffernan I just don't understand why it wouldn't work. It's not a big deal to implement a little detail like that. It should be be easy to check `FreeOnTerminate` one last time after `OnTerminate` is finished... Thank you for your info. – Ben Nov 25 '13 at 19:00
  • 2
    What problem are you trying to solve? – David Heffernan Nov 25 '13 at 19:02
  • @DavidHeffernan Problem has been solved. I just don't understand why it wouldn't work in the first place. – Ben Nov 25 '13 at 19:05
  • 2
    What problem are you trying to solve? – David Heffernan Nov 25 '13 at 19:27
  • 2
    What @David means: what condition desides on whether you finaly want to set `FreeOnTerminate` False? Because this sounds like [an XY-question](http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). – NGLN Nov 25 '13 at 20:41
  • 1
    @MasonWheeler published a framework for executing an anonymous procedure through a postmessage call. [`Delayed Action, revisited`](http://tech.turbu-rpg.com/454/delayed-action-revisited). Call it in your `OnTerminate` handler by `DelayExec( procedure begin TTestThread(Sender).Free; end);` – LU RD Nov 25 '13 at 22:37

3 Answers3

5

FreeOnTerminate is evaluated after Execute() exits but before DoTerminate() is called to trigger the OnTerminate event. You can change FreeOnTerminate until Execute() exits, then it is too late. So a workaround would be to trigger OnTerminate manually from inside of Execute(), eg:

type
  TTestThread = class (TThread)
  public
    procedure Execute; override;
    procedure DoTerminate; override;
  end;

procedure TTestThread.Execute;
begin
  try
    ...
  finally
    // trigger OnTerminate here...
    inherited DoTerminate;
  end;
end;

procedure TTestThread.DoTerminate;
begin
  // prevent TThread from triggering OnTerminate
  // again by not calling inherited here...
end;

The only gotcha is that if Terminate() is called before Execute() is called then TThread will skip Execute(), but it will still call the overridden DoTerminate().

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
4

If you take a look to the sources of ThreadProc in Classes.pas, you will find that FreeOnTerminate is evaluated to a local variable Freethread before calling the OnTerminate event in DoTerminate.
After calling DoTerminate the thread is freed depending of the now outdated variable: if FreeThread then Thread.Free;.
You could start the thread without FreeOnTerminate and use PostMessage with an own message e.g. WM_MyKillMessage (WM_APP + 1234) called in OnTerminate to free the thread after leaving the OnTerminate event.

This could look like:

const
  WM_KillThread = WM_APP + 1234;
type

  TTestThread = class (TThread)
  public
    procedure Execute; override;
    Destructor Destroy;override;
end;
  TForm2 = class(TForm)
    ............... 
  public
    { Public-Deklarationen }
    procedure WMKILLTHREAD(var Msg:TMessage);message WM_KillThread;
  end;


procedure TForm2.OnTestThread(Sender: TObject);
begin
  ShowMessage ('OnTestThread');
  PostMessage(handle,WM_KillThread, WPARAM(Sender), 0);
end;

procedure TForm2.WMKILLTHREAD(var Msg: TMessage);
begin
   TTestThread(Msg.WParam).Free;
end;

destructor TTestThread.Destroy;
begin
  ShowMessage ('Destroy');
  inherited;
end;
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
bummi
  • 27,123
  • 14
  • 62
  • 101
  • I really like the idea of posting a new message to the queue since `OnTerminate` is called in the context of the Mainthread. Thank you very much. – Ben Nov 25 '13 at 19:04
3

Presumably, you want to set FreeOnTerminate False in some sort of condition, but let it otherwise stay True. If that condition by any chance depends on whether its termination is natural (Execute ended normally without intervention) or manual (Terminate is called), then I suggest you do the exact opposite: create the thread with FreeOnTerminate = False and set it True when the Terminated property is False:

procedure TTestThread.Execute;
begin
  ...
  if not Terminated then
    FreeOnTerminate := True;
end;

See its functioning for example in When to free a Thread manually.

Community
  • 1
  • 1
NGLN
  • 43,011
  • 8
  • 105
  • 200