3

So I created a class: TWorkerThread = class(TThread). In the TWorkerThread Constructor I set FreeOnTerminate := True.

I then added a private Worker variable to my TForm. When I instantiate TWorkerThread for Worker, I always use CreateSuspended := False.

The only interactions I need to have with the thread once started is to cancel the process (Terminate) and check if the process is over (that check is user event based).

To check if the thread is still active I do:

if (Self.Worker <> nil) and (not Self.Worker.Finished) then
    //Still active

Which seems to work fine but I fear that at some time the TWorkerThread will be <> nil but still be .free (remember that TWorkerThread are all FreeOnTerminate := True). When a TWorkerThread "self destroy" once terminated do they always become nil? If not, how do I handle that (to avoid access violations)?

Another thing, if a Worker is active on it's form destroy, I do:

if (Self.Worker <> nil) and (not Self.Worker.Finished) then
begin
    Self.Worker.Terminate;
    //wait here for completion
end;

After the Terminate if I do a WaitFor I get an access violation (because of the self destroy). Should I enforce a wait here? Whats the best way to do it?

This is my first proper usage of TThread in Delphi. The code above is a skimmed down version of what I do for clarity.

AlexV
  • 22,658
  • 18
  • 85
  • 122
  • 1
    Once you start a free on terminate thread you can't call any method or reference any field from outside the thread because of race conditions. You need instead to get the thread to notify you when it is terminating. – David Heffernan Feb 03 '22 at 19:00
  • If you need to check for thread status or you need ability to terminate thread or anything similar you cannot use self destroying thread. There are not other viable options. – Dalija Prasnikar Feb 03 '22 at 19:16
  • @DavidHeffernan That is not entirely true. As long as 1. The thread has an OnTerminate event handler assiged and 2. The OnTerminate event handler nil the reference to the thread, you can still use the thread object from the main thread. The OnTerminate event is always executed in the context of the main thread, so as long as the thread reference is not nil, it will be safe to use the thread reference. Though one needs to be careful what functions of the thread are called to avoid deadlocks. – Ken Bourassa Feb 03 '22 at 20:02
  • 1
    @KenBourassa what you said doesn't change what David said. "*You need instead to get the thread to notify you when it is terminating... use `OnTerminate` to notify*" Do note, however, that a thread *could* override `TThread.DoTerminate()` to fire `OnTerminate` *without* the use of `TThread.Synchronize()` (I have threads that do exactly that), thus re-introducing the race condition on the thread reference that you say can't happen if you use `OnTerminate`. But that is a more advance usage, most people are not likely to do that most of the time, in which case what you said will work fine. – Remy Lebeau Feb 03 '22 at 21:16
  • @RemyLebeau I never challenged that part of David's point. I challenged the notion that you cannot use a thread reference if said thread is FreeOnTerminate, which is false. As for your second point, yeah, I guess you could override `TThread.DoTerminate()` to call `OnTerminate` without `TThread.Synchronize()`. Considering `TThread.OnTerminate` is explicitly documented as executing in the context of the main thread, we're not talking about TThread anymore, we're talking about `TYourThread`. But since it breaks the "contract" introduced by TThread, I'd introduce a new event for that instead. – Ken Bourassa Feb 07 '22 at 03:12

1 Answers1

3

What's the correct way in Delphi to check if a non-Suspended created TThread with FreeOnTerminate = True is still executing?

There is none.

As soon as self destroying thread is started, you are no longer allowed to touch reference holding such thread from outside code, because that reference can become stale at any time.

Under some circumstances (if logic handling OnTerminate call in DoTerminate is not modified in your custom thread class) you can safely access thread reference from OnTerminate event handler. However, that will not help you solve your problem because you cannot wait on self destroying thread. On Windows such code causes exception that can be caught and handled, but on other platforms it causes undefined behavior and random deadlocks. But even on Windows writing such code is not advisable.

If you need to check status of a thread, cancel it or wait for it, you cannot use self destroying threads. Period. (For those that may object previous statement, yes under some circumstances some of those things are possible, but they commonly lead to shooting yourself in the foot).

You can use following workflow with TWorkerThread, providing that you leave FreeOnTerminate on False in your thread constructor and construct unsuspended thread. All following methods must be called from the main thread to avoid race condition on Worker reference. Having one extra thread that does nothing while your form is open will not cause you any trouble.

procedure TMyForm.StartWorker;
begin
  FreeAndNil(Worker); // or if Assigned(Worker) then Exit;
  Worker := TWorkerThread.Create;
end;

procedure TMyForm.CancelWorker;
begin
  // this will initiate thread termination and waiting
  FreeAndNil(Worker);
end;

procedure TMyForm.Destroy;
begin
  // technically we could only use Free here, 
  // but since other code requires niling the reference this is more consistent 
  FreeAndNil(Worker);
  inherited;
end;
Dalija Prasnikar
  • 27,212
  • 44
  • 82
  • 159
  • Dalija Prasnikar, @RemyLebeau Thanks for you comments & answer, I stopped using self-destructing thread and manage it myself. I did a code review here if your are interested: https://codereview.stackexchange.com/questions/273776/am-i-correctly-managing-the-life-cycle-create-free-cancel-of-my-tthread-in-del – AlexV Feb 05 '22 at 18:04