Delphi:线程是否应该“非挂起”创建?

9

我一直在尝试追踪Jedi VCL中 JvHidControllerClass.pas 的内存泄漏问题,我在源代码历史记录中发现了以下更改:

旧版本:

constructor TJvHidDeviceReadThread.CtlCreate(const Dev: TJvHidDevice);
begin
  inherited Create(True);
  Device := Dev;
  NumBytesRead := 0;
  SetLength(Report, Dev.Caps.InputReportByteLength);
end;

当前版本:

constructor TJvHidDeviceReadThread.CtlCreate(const Dev: TJvHidDevice);
begin
  inherited Create(False);
  Device := Dev;
  NumBytesRead := 0;
  SetLength(Report, Dev.Caps.InputReportByteLength);
end;

根据我的经验,我发现如果你创建一个挂起的线程:

inherited Create(False);

然后线程立即开始运行。在这种情况下,它将尝试访问尚未初始化的对象:

procedure TJvHidDeviceReadThread.Execute;
begin
   while not Terminated do
   begin
     FillChar(Report[0], Device.Caps.InputReportByteLength, #0);
     if Device.ReadFileEx(Report[0], Device.Caps.InputReportByteLength, @DummyReadCompletion) then

立即尝试填充Report并访问对象Device。问题是它们尚未初始化; 这些是线程启动后的下一行代码:

  Device := Dev;
  NumBytesRead := 0;
  SetLength(Report, Dev.Caps.InputReportByteLength);

我认识到这是一种竞争条件;用户在生产环境中遇到崩溃的可能性非常低,因此保留这种竞争条件崩溃可能是无害的。

但是我有没有想错呢?我有没有忽略什么?调用以下内容:

BeginThread(nil, 0, @ThreadProc, Pointer(Self), Flags, FThreadID);

为什么线程不能立即开始运行?这真的是一个故意添加到JVCL中的竞争条件回归吗?有关此事是否有些秘密,请保留HTML标记。

CreateSuspended(False);

这使得它成为比以下代码更加正确的代码:

CreateSuspended(True);
...
FDataThread.Resume;

?

After having been burned by mistakenly calling

TMyThread.Create(False)

我把它记在脑海里,认为从来不正确。让线程立即启动(当您必须初始化值时)是否有任何有效的用途?

哇!JVCL 在 D5 上运行了!我以为在我退出并停止维护 D5 兼容性后,它已经被拔掉了。这种怀旧的感觉... - Arioch 'The
1
@Arioch'The 不要太怀旧了,这是2009年的JVCL 3.x。严格来说,这是Richard Marquand在2005年原创的HidController类,我稍微帮了一点忙。被JVCL采用的版本经历了巨大的“jcl-ifying”过程,但实际上并没有什么区别;但从技术上讲,我正在使用Richard的版本,所以我可以修复FastMM捕获的“use-after-free”崩溃。 - Ian Boyd
1个回答

12

这是 Delphi 5 中 TThread 实现的一个基本设计缺陷。底层的 Windows 线程在 TThread 构造函数中启动,这会导致您描述的竞争条件。

在 Delphi 6 版本的 RTL 中,线程启动机制已更改。从 Delphi 6 开始,线程在构造函数完成后的 TThread.AfterConstruction 中启动。这将使您的代码避免竞争条件。

在 Delphi 6 及更高版本中,底层的 Windows 线程是在 TThread 构造函数中创建的,但使用 CREATE_SUSPENDED 标志创建时是挂起的。然后在 AfterConstruction 中,只要 TThread.FCreateSuspended 不为 False,就会恢复线程。

解决 Delphi 5 中该问题的一种方法是最后调用继承的构造函数。像这样:

constructor TJvHidDeviceReadThread.CtlCreate(const Dev: TJvHidDevice);
begin
  Device := Dev;
  NumBytesRead := 0;
  SetLength(Report, Dev.Caps.InputReportByteLength);
  inherited Create(False);
end;

我知道这样说不太好听。

所以,你采用创建线程挂起并在构造函数完成后恢复的方法可能更好。这种方法反映了RTL如何解决Delphi 6及更高版本中的问题。


这解释了很多。历史课加一分! - Ian Boyd
我通常的做法是在线程执行中直接实例化/销毁。如果您需要使用诸如COM(例如ADO)之类的东西,那么无论如何都必须这样做。因此,实际上,每当我编写一个线程时,我从不在create/destroy中实现任何创建或销毁或其他任何事情。 (+1) - Jerry Dodge
@Jerry,这还好,直到你需要在创建者和线程之间进行通信。 - David Heffernan
“在 Delphi 5 中解决这个问题的一种方法是最后调用继承的构造函数”,另一种方法是复制 TThread 在 D6+ 中的做法。在你的构造函数中使用 CreateSuspended=True 调用 inherited(然后顺序无关紧要),然后重写 AfterConstruction() 来调用 Resume()。这样就不需要要求构造线程对象的调用代码手动调用 Resume() 了。 - Remy Lebeau

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