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;

并且变量在构造发生之前被赋值。这导致线程仅仅因为分配了变量就假定它是一个有效的对象。customer

Delphi(5)中会出现这种多线程单例的bug吗?


奖金问题

Delphi是否有一种公认的、线程安全的、一次性的初始化设计模式?许多人在 Delphi 中通过覆盖和实现单例;他们的实现将在多个线程中失败。NewInstanceFreeInstance

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

更新

两个人提出了一个包含常见错误的答案。损坏的双重检查锁定算法转换为 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;

来自维基百科

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


另一个错误的建议:

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

4 回答 4

8

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

我如何使用针对 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;

双重检查锁定是一种历史悠久的技术。最著名的讨论是“双重检查锁定被破坏”声明。这主要是在 Java 的上下文中设置的,所描述的问题不适用于您的情况(Delphi 编译器,x86 硬件)。事实上,对于 Java,随着 JDK5 的出现,我们现在可以说双重检查锁定是固定的。

Delphi 编译器不会根据对象的构造重新排序对单例变量的写入。更重要的是,强大的 x86 内存模型意味着处理器重新排序不会破坏这一点。请参阅谁在 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;
于 2012-05-30T08:27:26.840 回答
6

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

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

于 2012-05-29T20:12:56.573 回答
5

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

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

于 2012-05-29T22:49:30.240 回答
1

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

于 2012-05-30T08:41:26.283 回答