Does Delphi assign an instance variable before the object is fully constructed?
In other words, given a variable:
var
customer: TCustomer = nil;
we then construct a customer and assign it to the variable:
customer := TCustomer.Create;
Is it possible that customer
can be not nil
, but not point to a fully constructed TCustomer
?
This becomes a problem when performing lazy initialization:
function SacrifialCustomer: TCustomer;
begin
if (customer = nil) then
begin
criticalSection.Enter;
try
customer := TCustomer.Create;
finally
criticalSection.Leave;
end;
end;
Result := customer;
end;
The bug is in the line:
if (customer = nil)
It is possible that another thread calls:
customer := TCustomer.Create;
and the variable is assigned a value before construction happens. This causes the thread to assume that customer
is a valid object simply because the variable is assigned.
Can this multi-threaded singleton bug happen in Delphi (5)?
Bonus Question
Is there an accepted, thread-safe, one-time initialization design pattern for Delphi? Many people have implemented singletons in Delphi by overriding NewInstance
and FreeInstance
; their implementations will fail in multiple threads.
Strictly speaking i'm not after an answer on how to implement and singleton, but lazy-initialization. While singletons can use lazy-initialization, lazy initialization is not limited to singletons.
Update
Two people suggested an answer 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;
From Wikipedia:
Intuitively, this algorithm seems like an efficient solution to the problem. However, this technique has many subtle problems and should usually be avoided.
Another buggy suggestion:
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;
Update
i created some code and looked at the cpu window. It seems that this compiler, with my optimization settings, on this version of Windows, with this object, constructs the object first, then assigns the variable:
customer := TCustomer.Create;
mov dl,$01
mov eax,[$0059d704]
call TCustomer.Create
mov [customer],eax;
Result := customer;
mov eax,[customer];
Of course i cannot say that's guaranteed to always work that way.
hatchet
is safe on all known Delphi compilers on Windows on x86 and x64, due to the memory model on those platforms. I expect it will be safe on Mac OS on x86 too, but have no knowledge. – Glauconite