【问题标题】:Is a method pointer assignment thread safe?方法指针分配线程安全吗?
【发布时间】:2013-02-15 14:22:20
【问题描述】:

例子:

假设,我会有以下线程(请不要考虑这个例子的线程上下文执行方法中使用了什么,它只是为了解释):

type
  TSampleThread = class(TThread)
  private
    FOnNotify: TNotifyEvent;
  protected
    procedure Execute; override;
  public
    property OnNotify: TNotifyEvent read FOnNotify write FOnNotify;
  end;

implementation

procedure TSampleThread.Execute;
begin
  while not Terminated do
  begin
    if Assigned(FOnNotify) then
      FOnNotify(Self); // <- this method can be called anytime
  end;
end;

然后假设,我想随时从主线程更改OnNotify 事件的方法。该主线程在此处将事件处理程序方法实现为ThreadNotify 方法:

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    FSampleThread: TSampleThread;
    procedure ThreadNotify(Sender: TObject);
  end;

implementation

procedure TForm1.ThreadNotify(Sender: TObject);
begin
  // do something; unimportant for this example
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  FSampleThread.OnNotify := nil; // <- can this be changed anytime ?
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  FSampleThread.OnNotify := ThreadNotify; // <- can this be changed anytime ?
end;

问题:

更改一个方法是否安全,该方法可以随时从另一个线程的上下文中从工作线程调用?执行上述示例中的操作是否安全?

我不确定,如果这绝对安全,至少因为方法指针实际上是一对指针,我不知道我是否可以将其视为原子操作。

【问题讨论】:

  • 不要这样做,而是始终分配它并使用 32 位变量而不是整数或本机整数类型。然后为整数分配一个非零值或零值。如果整数值非零,则调用事件,否则不调用事件。这是一个简单的解决方法。
  • @bobshacks,这在多 CPU(或多核)系统上是不安全的,除非您每次分配该标记整数时都会造成内存屏障。由于没有障碍,一些 CPU 将在分配后一段时间内使用缓存中的“陈旧”数据。您需要 InterlockedXYZ 例程,但它可以工作。
  • 是的。使用 InterlockedExchange 函数进行分配。在这种情况下,InterlockedIncrement 和 InterlockedDecrement 更好。另一个想法是创建一个指向方法指针的指针。因此,将其作为表单中的变量,然后将指向该变量的指针传递给线程,自然而然地使用原子函数来执行此操作。那么它仍然只是一个指针。
  • @bobshacks,谢谢!我想我可以应付这种情况。这个问题我只是出于好奇而问的(实际上,我不会真的这样做),并希望它对其他人也有帮助,因为方法分配并不那么明显,人们可以忽略这种风险.
  • @CosminPrund 英特尔上的德尔福实际上不是问题。例如,双重检查锁定对于 Intel 上的 Delphi 工作得很好,正因为如此。英特尔 x86 和 x64 具有强大的内存模型,Delphi 不会优化或缓存来自全局可见的任何内容(即不是局部变量的所有内容)的读取和写入。

标签: multithreading delphi


【解决方案1】:

不,它不是线程安全的,因为该操作永远不会是“原子的”。 TNotifyEvent 由两个指针组成,这些指针永远不会同时被赋值:一个被赋值,另一个被赋值。

TNotifyEvent 赋值生成的 32 位汇编器由两条不同的汇编器指令组成,如下所示:

MOV [$00000000], Object
MOV [$00000004], MethodPointer

如果它是单个指针,那么您将有一些选项,因为该操作是原子的:您拥有的选项取决于 CPU 的内存模型有多强:

  • 如果 CPU 支持“顺序一致性”模型,那么在写入内存后发生的任何读取都会看到新值,这是有保证的。如果是这种情况,您可以简单地写入您的值,无需内存屏障或使用 Interlocked 方法。
  • 如果 CPU 对重新排序存储和加载更加放松,那么您需要一个“内存屏障”。如果是这种情况,最简单的解决方案是使用InterlockedExchangePointer

不幸的是,我不知道当前 Intel CPU 的内存模型有多强大。有一些间接证据表明可能会发生一些重新订购,建议使用Interlocked,但我还没有看到英特尔的明确声明说其中之一。

证据:

  • 现代 CPU 使用“预取” - 这自动意味着某种程度的加载/存储重新排序。
  • SSE 引入了处理 CPU 缓存的具体指令。

【讨论】:

  • 我就是这么想的。我只是想知道,如果编译器不以某种方式将它们 "magically" 视为一个指针。如果后面没有这种“单指针编译魔法”,那当然是不安全的。我可以自己检查这个反汇编(懒惰我)。感谢您的确认! [+1 并接受]
  • 内置的OnTerminate 事件是否有同样的风险?当然,每个线程生命周期只调用一次。但是 AFAICS RTL 不保护其分配。
  • @Sertac,OnTerminate方法是通过Synchronize调用的,至少在Delphi 2009中,所以它实际上是在主线程的上下文中执行的,所以没有风险。
  • @TLama - 我的问题不是调用分配的方法,而是分配给通知事件。
  • @SertacAkyuz,为什么OnTerminate 的行为会有所不同?它仍然是一个两点交易,编译器以相同的方式分配它。风险较低,因为它只被调用一次,即使那样它也是通过Synchronize 调用的;尽管如此,如果第三个线程(不是主线程,也不是终止线程)正在分配它,风险是绝对一样的。
【解决方案2】:

除了寄存器大小之外,还涉及两个操作。检查并稍后执行。要最小化,请创建一个本地 var 并使用它。但无论如何,这仍然不是 100% 线程安全的

var
  LNotify: TNotifyEvent;
begin
  ...
  LNotify := FOnNotify;
  if Assigned(LNotify) then
    LNotify(Self);
end;

【讨论】:

  • LNotify := FOnNotify 不是原子的。所以,没有骰子。
  • @David - 我想这是为了在“分配”测试之后和调用程序之前保护分配更改。
  • @Sertac 但这还不够,不是吗?
  • @Sertac 除了那个位是相当荒谬的。线程安全是非此即彼的。代码不能 90% 线程安全
  • @BasePointer Delphi 全局变量、记录字段、对象字段总是易变的。此答案中的代码毫无意义,因为数据是两个指针宽,因此无法原子访问。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-11-02
  • 1970-01-01
  • 2013-03-31
  • 1970-01-01
  • 2010-11-08
  • 1970-01-01
相关资源
最近更新 更多