Does Delphi assign the variable before the object is constructed?
Asked Answered
C

4

5

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.

Christenechristening answered 29/5, 2012 at 19:53 Comment(7)
https://mcmap.net/q/591030/-how-should-quot-double-checked-locking-quot-be-implemented-in-delphi I think this question is rather about double-checked locking idiom in delphi than about variable initialization.Fishtail
@BorisTreukhov That accepted code can suffer from the same problem depending on how the compiler behaves.Christenechristening
Yes, indeed :-(. #232575Fishtail
@IanBoyd - I have changed your implementation of the buggy temp variable suggestion to what I had in mind (hope you don't mind). Most likely, you are right that this still has problems but as I mentioned in comments, I don't grasp them.Brozak
@Lieven i was editing the question (and then tabbed away), which i tabbed back to save i blew away your changes. You can re-add them if you wish.Christenechristening
Never mind, you got the intent right. Where's David when you need him :)Brozak
@Lieven Well, I've quit because I'm too addicted. However, double checked locking as implemented by 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
G
8

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;
Glauconite answered 30/5, 2012 at 8:27 Comment(8)
There is also forth options "Busy-Wait Initialization" check my answer!Biometry
i'll use this. Although, for the sake of customers running Windows 2000, i'll use InterlockedCompareExchange - but the technique is the same. It wreaks havoc when it's ICustomer instead of TCustomer; but that's nothing that AddRef cannot fix.Christenechristening
Yeah, on D5, InterlockedCompareExchange is fine. InterlockedCompareExchangePointer is needed on x64.Glauconite
Is InterlockedCompareExchange not valid on 32-bit executables, even running on 64-bit Windows?Christenechristening
InterlockedCompareExchange is fine for 32 bit pointers. Your pointers are 32 bits wide.Glauconite
+1 I can see you're really taking the quitting thing serious :)Brozak
@Lieven One answer in 5 days, yes that's pretty serious.Glauconite
@DavidHeffernan - Hope to see you in 5 days then but remember: This is a dangerous time for you, when you will be tempted by the Dark Side of the ForceBrozak
F
6

Even if the assignment is made after construction, you still have the same problem. If two threads hit SacrifialCustomer at nearly the same time, both can execute the test if (customer = nil) before one of them enters the critical section.

One solution to that problem is double check locking (test again after entering the critical section). With Delphi this works on some platforms, but is not guaranteed to work on all platforms. Other solutions use static construction, which works in many languages (not sure about Delphi) because the static initialization only happens when the class is referenced, so it is in effect lazy, and static initializers are in inherently thread safe. Another is using a interlocked exchange which combines test and assignment into an atomic operation (for a Delphi example see the second answer here: How should "Double-Checked Locking" be implemented in Delphi?).

Fireeater answered 29/5, 2012 at 20:12 Comment(9)
This code will crash if Delphi performs variable assignment before object construction. if (customer=nil) is false, except it's not a valid customer object.Christenechristening
@IanBoyd - I don't have Delphi running here but surely you can verify that for yourself looking at the assembler code watching the customer variable getting a value. One sure way to prevent this problem in the first place is to always enter the critical section and live with the performance implication.Brozak
@IanBoyd - btw, did you read the link Boris provided? The Double Checked Locking mechanism can be broken depending on specific memory models used or (I would believe) compiler specific implementations (like assigning to customer before completing the construction (you have to love the irony :)).Brozak
@Lieven On the downside: " Since synchronizing a method can decrease performance by a factor of 100 or higher,[3] the overhead of acquiring and releasing a lock every time this method is called seems unnecessary: once the initialization has been completed, acquiring and releasing the locks would appear unnecessary."Christenechristening
@Lieven Not yet, i was about to. But first to say, "One of the dangers of using double-checked locking is that it will often appear to work: it is not easy to distinguish between a correct implementation of the technique and one that has subtle problems."Christenechristening
@IanBoyd - Just braindumping here but what about creating the customer into a temporary variable and assign that variable to customer after construction. Depending on data alignment, this would be an atomic operation.Brozak
@Lieven That's been looked into. Problem with that is the CPU is allowed to reorder memory operations - as long as the results are the same for the executing thread. That means bugs exist for multi-threading code.Christenechristening
@IanBoyd - That article mentions reordering when using two variables. I would think this doesn't apply here. The tempCustomer has to get it's value and thus complete the construction before this can be assigned to your real customer otherwise you would get a notable side-effect viewed from a single thread (wich can't happen).Brozak
@Lieven The real issue is that i'd hate to try to solve the multi-threaded singleton problem - only to reinvent the same bugs that have been solved already.Christenechristening
V
5

No, Delphi does not assign a value to the target variable before the constructor returns. Much of Delphi's library relies on that fact. (Objects' fields are initialized to nil; an unhandled exception in the object's constructor triggers its destructor, which is expected to call Free on all object fields that the constructor was assigning. If those fields had non-nil values, then further exceptions would occur.)

I elect not to address the bonus question because it's unrelated to the main question and because it's a much bigger topic than is appropriate for an afterthought.

Votive answered 29/5, 2012 at 22:49 Comment(3)
Sometimes people don't want to answer the question, but instead want to answer the "real" situation behind the question. This is not one of those times. This time, though, i wish i had a solution to my "real" problem. i'm down the rabbit hole; memory barries, reordering memory operations, NUMA, cache flushes. i just wanted a singleton :(Christenechristening
No rabbit hole here. Double checked locking works on x86 and x64. This is not Java. This is Delphi. Anyway, double checked locking is no longer broken in Java with volatile in JDK 5.Glauconite
It's not one of those times for me, at least. Everyone else seems keen on ignoring the main question and going straight to the harder one. But if that's what you want, Ian, then ask that question. You could have stopped after the motivating example (the part between the main question and the "bonus" one). It adequately shows why you're interested in the order of operations. If the variable is assigned early, then it won't work; if it's only assigned after construction, then it'll work in single-threaded programs. It's broken in multithreaded programs, with or without double-checked locking.Votive
B
1

Another solution to solve your problem is to use customer pointer as atomic lock variable which prevent multiple object creation. More about you can read at Busy-Wait Initialization Read also: On Optimistic and Pessimistic Initialization

Biometry answered 30/5, 2012 at 8:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.