13

What is a proper way to terminate a thread using Delphi for iOS under ARC management?

Take this simple example:

  TMyThread = class(TThread)
  protected
    procedure Execute; override;
  public
    destructor Destroy; override;
  end;

  TForm2 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
  private
    FThread: TMyThread;
  public
  end;

{ TMyThread }
destructor TMyThread.Destroy;
begin

  ShowMessage('Destroy');

  inherited Destroy;

end;

procedure TMyThread.Execute;
begin

  Sleep(5000);

end;

{ TForm2 }
procedure TForm2.Button1Click(Sender: TObject);
begin
  FThread := TMyThread.Create(TRUE);
  FThread.FreeOnTerminate := TRUE;
  FThread.Start;
end;

procedure TForm2.Button2Click(Sender: TObject);
begin
  ShowMessage(FThread.RefCount.ToString);
end;

procedure TForm2.Button3Click(Sender: TObject);
begin
  FThread := nil;
end;

Ok, pressing Button1 will spawn a thread. After thread is started, if you click Button2 it will display a RefCount value of 3!! Well, 1 is the reference to my FThread variable, and there are 2 additional references that TThread creates internally... I have digged into source code and found that RefCount is increased here:

constructor TThread.Create(CreateSuspended: Boolean);

  ErrCode := BeginThread(nil, @ThreadProc, Pointer(Self), FThreadID);
  if ErrCode <> 0 then
    raise EThread.CreateResFmt(@SThreadCreateError, [SysErrorMessage(ErrCode)]);
  {$ENDIF POSIX}

And here:

function ThreadProc(Thread: TThread): Integer;
var
  FreeThread: Boolean;
begin
  TThread.FCurrentThread := Thread;

Well... After thread is finished (In my case, after 5 seconds), the RefCount will decrease to 2 (Because I have set FreeOnTerminate to TRUE, but if I don´t set FreeOnTerminate to TRUE, the RefCount will still be 3).

See the problem? Thread is never finished and destructor is never called, if I call FThread := nil, then, the RefCount should decrease from 2 to 1 (Or from 3 to 2 in case FreeOnTerminate = FALSE), and thread is never released under ARC...

Maybe I´m missing something because I´m used to working with threads with no ARC... So, what am I missing here? Or is there a bug in TThread implementation under ARC?

Maybe this definition of TThread

private class threadvar
  FCurrentThread: TThread;

should be something like

private class threadvar
  [Weak] FCurrentThread: TThread;
Johan
  • 74,508
  • 24
  • 191
  • 319
Eric
  • 552
  • 3
  • 14
  • Isn't `FCurrentThread` released in the destructor? – Leonardo Herrera Jun 10 '13 at 19:42
  • FCurrentThread is a class threadvar variable of TThread, it is not a variable from the instance, but a class variable from TThread. This variable is never niled. Judging by the code, it should change if I spawn another thread... but I haven´t tested So... My thread destructor is never called – Eric Jun 10 '13 at 19:52
  • It won't change by spawning another thread. Each OS thread gets its own version of that variable because it's a threadvar. It's the TThread equivalent of GetCurrentThreadID. – Rob Kennedy Jun 10 '13 at 20:11
  • 2
    It's a shame you haven't got a complete program in the form of a console app to demo the issue. All that GUI just gets in the way, in my view. – David Heffernan Jun 10 '13 at 20:12
  • David, I can make that console app, if that helps... – Eric Jun 10 '13 at 20:17
  • 2
    From the source, I can see that `FCurrentThread` is never set to `nil`. I also see that `ThreadWrapper` pairs a `New` with `FreeMem` which is clearly bogus. `New` goes with `Dispose`. And since the thing that was allocated contained a reference to your thread, there could be a leak there. But since `Self` is cast to `Pointer`, that should be a weak ref. I'd cut it down to a minimal sample and submit a QC report. – David Heffernan Jun 10 '13 at 20:19
  • Oh, David, I can´t make a console app... It is Delphi for iOS, a Console app in Win32/64 or OSX does not implement ARC.... – Eric Jun 10 '13 at 20:21
  • Well.. I think I will fill up a QC report :) – Eric Jun 10 '13 at 20:23
  • OK, maybe not a console app. But all the code in a single file. No external resources (whatever .dfm files are calling in FMX) and so on. Just a file that starts `program` and has code executed between a top level `begin` and `end`. – David Heffernan Jun 10 '13 at 20:24
  • QC #116460 filled :) Now lets wait for Emb. answer – Eric Jun 10 '13 at 21:01
  • 1
    The real bug is that `ThreadProc()` does not nil `TThread.FCurrentThread` after the thread finishes running. That accounts for one dangling reference, at least. But I can't see why the refcount would be going up to 3. The `BeginThread()` code does not do anything that should be touching the refcount at all. – Remy Lebeau Jun 10 '13 at 21:44

1 Answers1

3

After some digging in qc the following issues and workaround show up:

Thread parameters should be passed as const

function ThreadProc(Thread: TThread): Integer;  <<-- pass_by_reference pushes
var                                                  up the ref_count.
  FreeThread: Boolean;
begin
  TThread.FCurrentThread := Thread;

Had you passed it as const the ref_count would have not gone up to 3. Normally this is not a problem, because the ref_count gets decreased on exit of the function, but here:

the function epilog is never exectued because pthread_exit() jumps out of the code.

This is only part of the solution though, quite a bit more needs to be done...

Full workaround by Dave Nottage

After much fiddling around, I've come up with this potential workaround:

Make these mods to the Classes unit: Change:

function ThreadProc(Thread: TThread): Integer;

to:

function ThreadProc(const Thread: TThread): Integer;

and add:

TThread.FCurrentThread := nil;

after this line:

if FreeThread then Thread.Free;

Override DoTerminate in the TThread descendant, thus:

procedure TMyThread.DoTerminate;
begin
  try
    inherited;
  finally
    __ObjRelease;
  end;
end;

Call the thread thus:

FMyThread.Free; // This will do nothing the first time around, since the reference will be nil
FMyThread := TMyThread.Create(True);
// DO NOT SET FreeOnTerminate
FMyThread.OnTerminate := ThreadTerminate;
FMyThread.Resume;

This (at least for me, on the device) results in the thread being destroyed on subsequent calls.

NOTE: Under ARC conditions, never declare a reference to a thread locally, because when it falls out of scope, the thread is destroyed and the Execute method is never called, not to mention other problems it causes.

Johan
  • 74,508
  • 24
  • 191
  • 319