5

In C#, the following code (from this page) can be used to lazily instantiate a singleton class in a thread safe way:

  class Foo {
        private volatile Helper helper = null;
        public Helper getHelper() {
            if (helper == null) {
                lock(this) {
                    if (helper == null)
                        helper = new Helper();
                }
            }
            return helper;
        }
    }

What would be the equivalent thread safe Delphi code?


The article also mentions two problems with Double Checked Locking in Java:

  • it is possible that the new object is constructed before the helper reference is made to point at the newly created object meaning that two objects are created
  • it is possible that the helper reference is made to point at a block of memory while the object is still being created meaning that a reference to an incomplete object will be returned

So while the code of the C# and the Java version in the mentioned article look almost identical, only the C# version works as expected. Which leads to the additional question if these two problems also exist in a Delphi version of Double-Checked Locking?

mjn
  • 36,362
  • 28
  • 176
  • 378

2 Answers2

7

Use System.TMonitor to lock the object instance in a thread safe way.

function TFoo.GetHelper(): THelper;
begin
  if not Assigned(FHelper) then
  begin
    System.MonitorEnter(Self);
    try
      if not Assigned(FHelper) then
        FHelper := THelper.Create();
    finally
      System.MonitorExit(Self);
    end;
  end;
  Result := FHelper;
end;

For further reference look at Lock my object..., please! from Allen Bauer. In fact, the rep. I gather from this should go to Allen.

Community
  • 1
  • 1
jachguate
  • 16,976
  • 3
  • 57
  • 98
  • 1
    I could not have said it better myself ;-) – Allen Bauer Dec 18 '10 at 00:02
  • I have the feeling that something essential is missing in this code: the C# implementation uses the `volatile` keyword for the private Helper variable. I guess that FHelper **must** be declared as `threadvar`? – mjn Dec 18 '10 at 11:24
  • threadvar would result in one per thread rather than a singleton shared by all threads. The volatile may be due to .net memory model. But the code above is correct on x86. – David Heffernan Dec 18 '10 at 11:55
  • @mjn: I'm not a .NET expert, by the [C# volatile reference](http://msdn.microsoft.com/en-us/library/x13ttww7.aspx) it's clear that threadvar is not needed here, as said by @David Heffernan. IMHO using Delphi there is no need for such keyword because the _"most up to date value"_ is present in the field all the times, obviously protecting the multithread memory access with a Synchronization object, like monitor, critical section, TMultiReadExclusiveWriteSynchronizer or such. – jachguate Dec 18 '10 at 14:38
  • @jachgate The volatile statement in C# is there to force the compiler to erect memory barriers to prevent the re-ordering that breaks double-checked locking - see my answer. – David Heffernan Dec 18 '10 at 19:18
  • @David Heffernan: I tried several times, but the linked page just don't respond, maybe the access is blocked or we just are in very different time-zones and the host is working office hours. Anyway, I found it using google cache now and I understand what volatile means, thanks. – jachguate Dec 20 '10 at 17:21
2

Of course, it's always worth remembering that Double-Checked Locking is Broken. This issue turns out not to apply to the x86 memory model but it's always worth bearing in mind for the future. I'm sure there will be Delphi version at some point that will run on a platform with a memory model that is afflicted by this issue.

Embarcadero have started using a lock-free version of this pattern with interlocked compare/exchange. For example:

class function TEncoding.GetUnicode: TEncoding;
var
  LEncoding: TEncoding;
begin
  if FUnicodeEncoding = nil then
  begin
    LEncoding := TUnicodeEncoding.Create;
    if InterlockedCompareExchangePointer(Pointer(FUnicodeEncoding), LEncoding, nil) <> nil then
      LEncoding.Free;
  end;
  Result := FUnicodeEncoding;
end;

I realise this isn't an answer to the question but it didn't really fit in a comment!

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Is Double-Checked Locking *always* broken? The mentioned article says that the C# example above 'works as expected'. – mjn Dec 18 '10 at 11:35
  • It depends on the memory model in use. It works on x86 but I'm not sure about x64. It's broken on Java which has its own memory model. It's very complex though. – David Heffernan Dec 18 '10 at 11:38
  • x86 and x64 use the same 'strong' memory model, however Itanium is a little different according to http://igoro.com/archive/volatile-keyword-in-c-memory-model-explained/ – mjn Dec 18 '10 at 11:46
  • i'm not sure about .net and I have a feeling the memory model changed a few versions back – David Heffernan Dec 18 '10 at 11:53
  • Rergarding Java: the article linked in your answer also says "As of JDK5, there is a new Java Memory Model and Thread specification." ... "With this change, the Double-Checked Locking idiom can be made to work by declaring the helper field to be volatile." – mjn Feb 06 '12 at 10:39