【问题标题】:Does Delphi assign the variable before the object is constructed?Delphi 是否在构造对象之前分配变量?
【发布时间】:2012-06-04 01:37:10
【问题描述】:

Delphi 是否在对象完全构造之前分配实例变量?

换句话说,给定一个变量:

var
   customer: TCustomer = nil; 

然后我们构造一个客户并将其分配给变量:

customer := TCustomer.Create;

是否有可能customer 不能不是nil,但不能指向完全构造的TCustomer?


这在执行延迟初始化时会出现问题:

function SacrifialCustomer: TCustomer;
begin
   if (customer = nil) then
   begin
      criticalSection.Enter;
      try
         customer := TCustomer.Create;
      finally 
         criticalSection.Leave;
      end;
   end;
   Result := customer;
end;

bug就在一行:

if (customer = nil) 

有可能是另一个线程调用:

customer := TCustomer.Create;

and the variable is assigned a value before construction happens。这会导致线程假设 customer 是一个有效的对象,因为变量被赋值了。

这种多线程单例的bug会在Delphi(5)中发生吗?


奖金问题

Delphi 是否有公认的、线程安全的one-time initialization 设计模式?许多人通过覆盖NewInstance 和FreeInstance 在Delphi 中实现了singletons;它们的实现将在多个线程中失败。

严格来说,我不是在回答如何实现和单例,而是延迟初始化。虽然单例可以使用延迟初始化,但延迟初始化不限于单例。

更新

两个人建议了一个答案that contains a common mistake. The broken double-checked locking algorithm translated to Delphi:

// Broken multithreaded version
// "Double-Checked Locking" idiom
if (customer = nil) then
begin
   criticalSection.Enter;
   try
      if (customer = nil) then
         customer := TCustomer.Create;
   finally
      criticalSection.Leave;
   end;
end;
Result := customer;

来自Wikipedia:

直观地说,这个算法似乎是解决问题的有效方法。但是,这种技术有许多微妙的问题,通常应该避免。


另一个错误的建议:

function SacrificialCustomer: TCustomer;
var
  tempCustomer: TCustomer;
begin
   tempCustomer = customer;
   if (tempCustomer = nil) then
   begin
      criticalSection.Enter;
      try
         if (customer = nil) then
         begin
            tempCustomer := TCustomer.Create;
            customer := tempCustomer;
         end;
      finally
         criticalSection.Leave;
      end;
   end;
   Result := customer;
end;

更新

我创建了一些代码并查看了 cpu 窗口。看来这个编译器,用我的优化设置,在这个版本的windows上,用这个对象,先构造对象,然后赋值变量:

customer := TCustomer.Create;
       mov dl,$01
       mov eax,[$0059d704]
       call TCustomer.Create
       mov [customer],eax;
Result := customer;
       mov eax,[customer];

当然,我不能说一定会一直这样工作。

【问题讨论】:

  • stackoverflow.com/questions/4475080/how-should-double-checked-locking-be-implemented-in-delphi 我认为这个问题是关于 delphi 中的双重检查锁定习语而不是关于变量初始化。
  • @BorisTreukhov 接受的代码可能会遇到同样的问题,具体取决于编译器的行为方式。
  • @IanBoyd - 我已将您对 buggy 临时变量建议的实现更改为我的想法(希望您不介意) .最有可能的是,这仍然存在问题,但正如我在 cmets 中提到的那样,我没有掌握它们。
  • @Lieven 好吧,我已经戒烟了,因为我太上瘾了。但是,由于这些平台上的内存模型,hatchet 实现的双重检查锁定在 x86 和 x64 上的 Windows 上所有已知的 Delphi 编译器上都是安全的。我希望它在 x86 上的 Mac OS 上也是安全的,但不知道。

标签: delphi lazy-loading delphi-5


【解决方案1】:

我读到你的问题是你在问这个:

如何使用针对 x86 硬件的 Delphi 5 实现单例的线程安全延迟初始化。

据我所知,您有三个选择。

1.使用锁

function GetCustomer: TCustomer;
begin
  Lock.Acquire;
  try
    if not Assigned(Customer) then // Customer is a global variable
      Customer := TCustomer.Create;
    Result := Customer;
  finally 
    Lock.Release;
  end;
end;

这样做的缺点是如果GetCustomer 存在争用,那么锁的序列化将抑制缩放。我怀疑人们对这一点的担心超出了必要的程度。例如,如果您有一个执行大量工作的线程,该线程可以获取对单例的引用的本地副本以减少争用。

procedure ThreadProc;
var
  MyCustomer: TCustomer;
begin
  MyCustomer := GetCustomer;
  // do lots of work with MyCustomer
end;

2。双重检查锁定

这是一种允许您在创建单例后避免锁争用的技术。

function GetCustomer: TCustomer;
begin
  if Assigned(Customer) then
  begin
    Result := Customer;
    exit;
  end;

  Lock.Acquire;
  try
    if not Assigned(Customer) then
      Customer := TCustomer.Create;
    Result := Customer;
  finally 
    Lock.Release;
  end;
end;

双重检查锁定是一种历史悠久的技术。最著名的讨论是The "Double-Checked Locking is Broken" Declaration。这主要是在 Java 的上下文中设置的,所描述的问题不适用于您的情况(Delphi 编译器,x86 硬件)。确实,对于 Java,随着 JDK5 的出现,我们现在可以说双重检查锁定是固定的。

Delphi 编译器不会根据对象的构造重新排序对单例变量的写入。更重要的是,强大的 x86 内存模型意味着处理器重新排序不会破坏这一点。见Who ordered memory fences on an x86?

简单地说,Delphi x86 上没有破坏双重检查锁定。更重要的是,x64 内存模型也很强大,双重检查锁定也没有被破坏。

3。比较和交换

如果您不介意创建单例类的多个实例,然后丢弃除一个之外的所有实例,您可以使用比较和交换。 VCL 的最新版本利用了这种技术。它看起来像这样:

function GetCustomer;
var
  LCustomer: TCustomer;
begin
  if not Assigned(Customer) then 
  begin
    LCustomer := TCustomer.Create;
    if InterlockedCompareExchangePointer(Pointer(Customer), LCustomer, nil) <> nil then
      LCustomer.Free;
  end;
  Result := Customer;
end;

【讨论】:

  • 还有四个选项“Busy-Wait Initialization”检查我的答案!
  • 我会用这个。虽然,为了运行 Windows 2000 的客户,我将使用InterlockedCompareExchange - 但技术是相同的。当它是ICustomer 而不是TCustomer 时,它会造成严重破坏;但这不是AddRef 无法解决的问题。
  • 是的,在 D5 上,InterlockedCompareExchange 很好。 x64 上需要InterlockedCompareExchangePointer。
  • InterlockedCompareExchange 在 32 位可执行文件上无效,即使在 64 位 Windows 上运行?
  • InterlockedCompareExchange 适用于 32 位指针。你的指针是 32 位宽的。
【解决方案2】:

即使是在构造之后进行分配,你仍然有同样的问题。如果两个线程几乎同时命中 SacrifialCustomer,则两个线程都可以在其中一个进入临界区之前执行测试if (customer = nil)。

该问题的一个解决方案是双重检查锁定(进入关键部分后再次测试)。使用 Delphi,这适用于某些平台,但不能保证适用于所有平台。其他解决方案使用静态构造,它适用于许多语言(不确定 Delphi),因为静态初始化仅在引用类时发生,因此它实际上是惰性的,并且静态初始化器本质上是线程安全的。另一种是使用联锁交换,它将测试和赋值组合成一个原子操作(对于 Delphi 示例,请参见此处的第二个答案:How should "Double-Checked Locking" be implemented in Delphi?)。

【讨论】:

  • 这段代码会崩溃 if Delphi 在对象构造之前执行变量赋值。 if (customer=nil) 为 false,除非它不是有效的客户对象。
  • @IanBoyd - 我没有在这里运行 Delphi,但您肯定可以通过查看汇编代码观察客户变量获取值来验证这一点。首先防止此问题的一个可靠方法是始终进入关键部分并接受性能影响。
  • @IanBoyd - 顺便说一句,你读过鲍里斯提供的链接吗?双重检查锁定机制可以根据使用的特定内存模型或(我相信)编译器特定实现(例如在完成构造之前分配给客户(你必须喜欢讽刺:)).
  • @Lieven On the downside: " 由于同步一个方法会使性能降低 100 倍或更高,[3] 每次此方法获取和释放锁的开销为调用似乎没有必要:一旦初始化完成,获取和释放锁似乎是不必要的。”
  • @Lieven 真正的问题是我不想尝试解决多线程单例问题 - 只是为了重新发明已经解决的相同错误。
【解决方案3】:

不,Delphi 不会在构造函数返回之前为目标变量赋值。 Delphi 的大部分库依赖这一事实。 (对象的字段被初始化为 nil;对象的构造函数中未处理的异常会触发其析构函数,预计会在构造函数分配的所有对象字段上调用 ​​Free。如果这些字段具有非 nil 值,则进一步的异常会发生。)

我选择不解决附加问题,因为它与主要问题无关,而且它是一个比事后思考更合适的话题。

【讨论】:

  • 有时人们不想回答问题,而是想回答问题背后的“真实”情况。这不是那些时代之一。不过,这一次,我希望我能解决我的“真正”问题。我在兔子洞里;内存屏障、重新排序内存操作、NUMA、缓存刷新。 我只想要一个单身人士:(
  • 这里没有兔子洞。双重检查锁定适用于 x86 和 x64。这不是Java。这是德尔福。无论如何,在 JDK 5 中使用 volatile 的 Java 不再破坏双重检查锁定。
  • 至少对我来说不是那种时代。其他所有人似乎都热衷于忽略主要问题并直接进入更难的问题。但如果这就是你想要的,Ian,那就问那个问题。你本可以在激励示例之后停下来(主要问题和“奖励”问题之间的部分)。它充分说明了为什么您对操作顺序感兴趣。如果变量被提前分配,那么它将不起作用;如果它只是在构造之后分配,那么它将在单线程程序中工作。它在多线程程序中被破坏,有或没有双重检查锁定。
【解决方案4】:

解决您的问题的另一个解决方案是使用customer 指针作为防止创建多个对象的原子锁变量。 更多关于你可以阅读Busy-Wait Initialization 另请阅读:On Optimistic and Pessimistic Initialization

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-10-09
    • 2017-02-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-28
    • 2012-05-16
    相关资源
    最近更新 更多