Does Delphi assign the variable before the object

2020-07-16 02:20发布

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.

4条回答
等我变得足够好
2楼-- · 2020-07-16 03:07

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

查看更多
戒情不戒烟
3楼-- · 2020-07-16 03:15

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?).

查看更多
做个烂人
4楼-- · 2020-07-16 03:23

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.

查看更多
劳资没心,怎么记你
5楼-- · 2020-07-16 03:26

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;
查看更多
登录 后发表回答