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设计模式?许多人通过覆盖NewInstance
和FreeInstance
在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];
当然,我不能说保证始终以这种方式工作。
答案 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