Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delphi TThread under ARC (iOS) not being released

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;
like image 615
Eric Avatar asked Jun 10 '13 19:06

Eric


1 Answers

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.

like image 51
Johan Avatar answered Oct 21 '22 12:10

Johan