Delphi是否在构造对象之前分配变量?

时间:2012-05-29 19:53:41

标签: delphi lazy-loading delphi-5

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;

错误在于:

if (customer = nil) 

另一个线程可能会调用:

customer := TCustomer.Create;

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

这个多线程单例错误可以在Delphi(5)中发生吗?


奖金问题

Delphi是否有可接受的,线程安全的one-time initialization设计模式?许多人通过覆盖NewInstanceFreeInstance在Delphi中实现了单身;他们的实现将在多个线程中失败。

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

更新

两个人建议回答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];

当然,我不能说保证始终以这种方式工作。

4 个答案:

答案 0 :(得分:9)

我对你的问题的解读是你问这个问题:

  

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

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

<强> 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的出现,我们现在可以说Double-Checked Locking是固定的。

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

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

第3。比较和交换

如果您不介意创建单个类的多个实例,然后丢弃除一个以外的所有实例,则可以使用compare和swap。最新版本的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;

答案 1 :(得分:6)

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

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

答案 2 :(得分:5)

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

我选择不解决奖金问题,因为它与主要问题无关,因为这是一个比事后想法更合适的话题。

答案 3 :(得分:1)

解决问题的另一个解决方案是使用customer指针作为原子锁变量来阻止多个对象的创建。 有关您的更多信息,请参阅Busy-Wait Initialization 另请阅读:On Optimistic and Pessimistic Initialization