Low-Lock Singletons In D
davesdprogramming.wordpress.com
davesdprogramming.wordpress.com
Somewhat less problematic is that fast TLS is not natively available on all architectures (in particular, Mac OS X). Luckily, this can be worked around relatively easily.
psuedocode:
member atomic32 init
member mutex lock
member obj ptr
membarrier()
if (atomic_load(&init)) {
return ptr
} else {
locked_init()
atomic_save(&init, 1)
membarrier()
}
Not an expert, use this at your own risk.I.e. reads could otherwise get re-ordered so that you see the pointer and the not-yet-initialized object at the same time.
Edit: Also, the memory barriers are in the wrong place in the code. You need a read barrier between the init check and returning ptr, and a write barrier between the actual initialization of the object and writing the init variable.
On those architectures with weaker models the real simpler thing to do is to make the singleton thread-unsafe for initialization.
In Java, this is how a threadsafe Singleton looks like:
private static MySingleton instance = new MySingleton();
It will be initialized when the class is first used.The whole double-checked locking and TLS thing is IMO a huge pointless smartassery contest in VM lawyering hinging on the eminently false premise that having singletons in a heavily multithreaded environment initialized sooner than they're used is a problem that anyone needs solved.
Whereas in reality, 90% of all applications don't need lazy loading, and for 90% of those that do, the above is lazy enough.
Consider a global flag that gets instantiated and synchronized across threads/cores to a state of 0 such that if it ever gets set to 1, it was after publishing and synchronizing the shared instance via a global pointer. This pointer, and the global flag, will not revert for the rest of the singleton's life (noted in the article) - so there is nothing to synchronize past that - and it is only for the guarding flag's 0 (unallocated) case that there is a question if things are synchronized. That check and potential allocation are what need a standard lock (provided by D's syncronized block), which the example already provides.
The problem here is that the memory accesses for the path that bypasses the synchronization block are not guaranteed to occur in the correct order without barriers.
(Edited)
Edit: (Not waiting for reply link to appear...) When I say "the memory barriers could also be skipped" I am referring to any other memory barriers past those associated with initialization, not those associated with initialization. There would be memory barriers at initialization, but that is the only need for memory barriers and I believe that is provided here by the synchronized keyword. The object presented in the article then looks like a cache-friendlier version of a single global variable with the necessary barriers limited to the initialization of the global instance pointer (which might also be done as in the Java example given in the comments on the page). The TLS does not eliminate the need for any barriers, only helps eliminate non-local reads. That might allow a performance gain depending on the cache behavior (or, for example, in the presence of NUMA), but it hasn't reduced the need for locking IMHO.
There is no way around this. Your statement that memory barriers could be skipped is exactly the source of the double checked locking bug. I know this is hard to understand, which is why it has tripped up a looong list of very smart people over the years.
David's article solves the problem by having one of the globals not be global at all, but thread local.
As for performance, David's article shows with benchmarks that his solution is a huge win.
The problem is, in any event, not the synchronization, but the common path -- for which double-checked locking tries to avoid synchronization --, which needs at a minimum a load-load barrier. That can be done efficiently on TSO architectures (where a load-load barrier is a no-op), but is not guaranteed to be fast on other architectures.