Delphi TThread在ARC(iOS)下未被释放

13

在使用Delphi for iOS下进行ARC管理时,如何正确终止线程?

请参考以下简单示例:

  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;

好的,按下Button1将会生成一个线程。线程启动后,如果您点击Button2,它将显示一个RefCount值为3!!其中1是对我的FThread变量的引用,还有2个TThread内部创建的附加引用... 我已经深入研究了源代码,并发现RefCount在这里增加:

constructor TThread.Create(CreateSuspended: Boolean);

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

还有这里:

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

嗯... 在线程结束后(在我的情况下,5秒钟后),RefCount将会减少到2(因为我已将FreeOnTerminate设置为TRUE,但如果我没有设置FreeOnTerminate为TRUE,则RefCount仍将是3)。

看到问题了吗?如果调用FThread := nil,那么RefCount应该从2减少到1(或者在FreeOnTerminate = FALSE的情况下从3减少到2),并且在线程下使用ARC时,线程永远不会被释放......

也许我错过了什么,因为我习惯于处理没有ARC的线程......那么我在这里错过了什么?或者是TThread在ARC下的实现中存在bug吗?

也许是TThread的定义问题。

private class threadvar
  FCurrentThread: TThread;

应该是这样的

private class threadvar
  [Weak] FCurrentThread: TThread;

FCurrentThread是TThread的一个类threadvar变量,它不是实例的变量,而是TThread的类变量。这个变量从来没有被置为nil。根据代码判断,如果我创建另一个线程,它应该会改变...但我还没有测试。所以...我的线程析构函数从未被调用。 - Eric
2
很遗憾你没有一个完整的控制台应用程序来演示问题。在我看来,所有那些GUI只会妨碍事情的进行。 - David Heffernan
2
从源代码中可以看出,FCurrentThread 从未被设置为 nil。我还注意到 ThreadWrapperNewFreeMem 配对使用,这显然是错误的。New 应该与 Dispose 配对使用。而且,由于分配的对象包含对线程的引用,可能会导致内存泄漏。但是由于 Self 被强制转换为 Pointer,所以应该是一个弱引用。我会将其简化为一个最小示例,并提交一个 QC 报告。 - David Heffernan
QC #116460已填写 :) 现在让我们等待Emb的答复。 - Eric
1
真正的错误在于ThreadProc()在线程运行结束后没有将TThread.FCurrentThread设置为nil。这至少会导致一个悬空引用。但我不明白为什么引用计数会增加到3。BeginThread()代码并没有做任何应该影响引用计数的事情。 - Remy Lebeau
显示剩余6条评论
1个回答

3

经过在QC(Quality Center)的一些调查,以下问题和解决方法浮出水面:

线程参数应该作为const传递。

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

如果你将其作为const传递,ref_count就不会增加到3。通常这不是问题,因为在函数退出时,ref_count会减少,但在这里:函数epilog从未执行,因为pthread_exit()跳出了代码。尽管如此,这只是解决方案的一部分,还需要做更多工作...通过Dave Nottage的完整解决方案,我经过很多摸索,想出了这个潜在的解决方法:对Classes单元进行以下修改:将:
function ThreadProc(Thread: TThread): Integer;

to:

function ThreadProc(const Thread: TThread): Integer;

并且添加:

TThread.FCurrentThread := nil;

在这行之后:

if FreeThread then Thread.Free;

在TThread派生类中覆盖DoTerminate,如下所示:

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

将线程命名为:

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;

这会导致线程在后续调用时被销毁(至少对我来说,在该设备上是如此)。

注意:在 ARC 条件下,永远不要在本地声明对线程的引用,因为当其超出作用域时,线程会被销毁,Execute 方法也不会被调用,更不用说它引发的其他问题了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接