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)
当然,我不能说保证始终以这种方式工作.
我对你的问题的解读是你在问这个:
我如何使用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)
即使在施工后进行了分配,您仍然会遇到同样的问题.如果两个线程几乎同时命中SacrifialCustomer,则两个线程都可以if (customer = nil)在其中一个进入临界区之前执行测试.
该问题的一个解决方案是双重检查锁定(在进入临界区后再次测试).使用Delphi,这适用于某些平台,但不保证可以在所有平台上运行.其他解决方案使用静态构造,它可以在许多语言中工作(不确定Delphi),因为静态初始化仅在引用类时发生,因此它实际上是惰性的,而静态初始化器本身就是线程安全的.另一个是使用互锁交换,它将测试和分配结合到一个原子操作中(对于Delphi示例,请参见第二个答案:如何在Delphi中实现"双重检查锁定"?).
不,Delphi在构造函数返回之前不会为目标变量赋值.Delphi的大部分库都依赖于这个事实.(对象的字段初始化为nil;对象构造函数中的未处理异常触发其析构函数,预期它将调用Free构造函数指定的所有对象字段.如果这些字段具有非零值,则会发生进一步的异常. )
我选择不解决奖金问题,因为它与主要问题无关,因为这是一个比事后想法更合适的话题.
| 归档时间: |
|
| 查看次数: |
1395 次 |
| 最近记录: |