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

Ian*_*oyd 5 delphi lazy-loading delphi-5

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

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

var
   customer: TCustomer = nil; 
Run Code Online (Sandbox Code Playgroud)

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

customer := TCustomer.Create;
Run Code Online (Sandbox Code Playgroud)

是否可能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;
Run Code Online (Sandbox Code Playgroud)

错误在于:

if (customer = nil) 
Run Code Online (Sandbox Code Playgroud)

另一个线程可能会调用:

customer := TCustomer.Create;
Run Code Online (Sandbox Code Playgroud)

并且在构造发生之前为变量赋值.这会导致线程假定customer是一个有效的对象,因为该变量已分配.

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


奖金问题

Delphi 是否有可接受的,线程安全的一次性初始化设计模式?许多人通过覆盖而在德尔福实施了单身人士 ; 他们的实现将在多个线程中失败.NewInstanceFreeInstance

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

更新

两个人提出了一个包含常见错误的答案.破碎双重检查锁定算法转换为德尔福:

// 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;
Run Code Online (Sandbox Code Playgroud)

来自维基百科:

直觉上,这种算法似乎是解决问题的有效方法.然而,这种技术有许多微妙的问题,通常应该避免.


另一个错误的建议:

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;
Run Code Online (Sandbox Code Playgroud)

更新

我创建了一些代码并查看了cpu窗口.看来这个带有我的优化设置的编译器在这个版本的Windows上,使用这个对象,首先构造对象,然后分配变量:

customer := TCustomer.Create;
       mov dl,$01
       mov eax,[$0059d704]
       call TCustomer.Create
       mov [customer],eax;
Result := customer;
       mov eax,[customer];
Run Code Online (Sandbox Code Playgroud)

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

Dav*_*nan 8

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

我如何使用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;
Run Code Online (Sandbox Code Playgroud)

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

procedure ThreadProc;
var
  MyCustomer: TCustomer;
begin
  MyCustomer := GetCustomer;
  // do lots of work with MyCustomer
end;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

双重检查锁定是一种具有相当方格历史的技术.最着名的讨论是"双重锁定破损"声明.这主要是在Java的上下文中设置的,所描述的问题不适用于您的情况(Delphi编译器,x86硬件).实际上,对于Java,随着JDK5的出现,我们现在可以说Double-Checked Locking是固定的.

Delphi编译器不会根据对象的构造对单例变量进行重新排序.更重要的是,强大的x86内存模型意味着处理器重新排序不会破坏这一点.请参阅谁在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;
Run Code Online (Sandbox Code Playgroud)


hat*_*ica 6

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

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

  • @Lieven真正的问题是我不想尝试解决多线程单例问题 - 只能重新发明已经解决的相同错误. (2认同)

Rob*_*edy 5

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

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