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.
My reading of your question is that you are asking this:
How can I, using Delphi 5 targeting x86 hardware, implement thread-safe lazy initialization of a singleton.
To the best of my knowledge you have three options.
1. Use a lock
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;
The downside of this is that if there is contention on GetCustomer
then the serialization of the lock will inhibit scaling. I suspect that people worry about that a lot more than is necessary. For example, if you have a thread that performs a lot of work, that thread can take a local copy of the reference to the singleton to reduce the contention.
procedure ThreadProc;
var
MyCustomer: TCustomer;
begin
MyCustomer := GetCustomer;
// do lots of work with MyCustomer
end;
2. Double checked locking
This is a technique that allows you, once the singleton has been created, to avoid the lock contention.
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;
Double checked locking is a technique with a rather chequered history. The most famous discussion is The "Double-Checked Locking is Broken" Declaration. This is set mostly in the context of Java and the problems described do not apply to your situation (Delphi compiler, x86 hardware). Indeed, for Java, with the advent of JDK5, we can now say that Double-Checked Locking is Fixed.
The Delphi compiler doesn't re-order the write to the singleton variable with respect to the construction of the object. What's more, the strong x86 memory model means that processor re-orderings don't break this. See Who ordered memory fences on an x86?
Simply put, double checked locking is not broken on Delphi x86. What's more, the x64 memory model is also strong and double checked locking is not broken there either.
3. Compare and swap
If you don't mind the possibility of creating multiple instances of the singleton class, and then discarding all but one, you can use compare and swap. Recent versions of the VCL make use of this technique. It looks like this:
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;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With