Hacker News new | ask | show | jobs
by skrap 3991 days ago
Totally! I was going to post the same thing. Double-checked locking is either impossible or really hard to get right, depending on the language and architecture's guarantees!

If you must use a singleton, I'd really recommend doing the so-called "aggressive" approach, which should have really been named the "actually won't crash sometimes" approach.

3 comments

+1 for the aggressive approach, mutexes are very fast in go.

I got curious and wrote a quick little benchmark test:

    $ cat bench_test.go
    package main
    
    import (
    	"sync"
    	"testing"
    )
    
    func BenchmarkMutex(b *testing.B) {
    	var m sync.Mutex
    	for n := 0; n < b.N; n++ {
    		m.Lock()
    		m.Unlock()
    	}
    }


    $ go test -v -bench=. bench_test.go
    BenchmarkMutex	50000000	        24.0 ns/op
    ok  	command-line-arguments	1.235s
A set of mutex.Lock() & .Unlock() calls takes only 24.0ns on average to complete.

Thus it's possible to lock/unlock more than 41 million times per second on the puny 2011 MacBook Air I used for this.

My .02c:

The post seems like a case of premature optimization.

Resource bottlenecks due to too much mutex locking in go does not seem like a case that will be commonly hit.

With infinite potential bottlenecks, I don't like to spend my time worrying and fussing over things that are:

A) Not yet a problem.

B) Unlikely to ever be a problem or give me grief.

Worrying about the overhead cost of locking falls squarely into just such a category.

Be careful with that benchmark. It's very vulnerable to SROA and constant propagation optimizing it away to nothing. (Doesn't look like Go's compiler optimizations are able to do that based on those numbers, but a modern optimizer will.)
Here's the mutex source: https://golang.org/src/sync/mutex.go . The fast path is just a CAS, which of course is going to be fast. But it's also important to know how it performs under contention.
This has no contention and is essentially a no-op? Or do I not understand go?
It is a no-op, but Golang's Plan 9-based compilers don't do much optimization, so they aren't able to eliminate it.
Try again with many goroutines trying to lock/unlock in parallel (and GOMAXPROCS>1), the results are quite different!
On C++ if we're using a bool as the "check" and pthread_mutex_lock on a regular mutex would that work, or do we still need to hardcode fences?

(Asking because this pattern is used by libcxxabi code)

A few years ago, the C++ standard didn't guarantee that the double-checked idiom would work. Even if your threading library had some way to insert the necessary memory barriers, it was possible that your compiler would optimize important things away.

C++11 changes that. The language now has standard ways to add memory barriers, and compiler optimizations can't remove them.

Yes, this is broken on certain architectures like PPC. Another core may see the "check" bool as set, but the fields of the protected object as uninitialized. One way to address this is to insert a write barrier just before setting the check bool, and a read barrier just after reading it.
In modern Java, it's easy enough if you've been shown the pattern. What's hard is understanding/discovering it from first principles.
Someone apparently disliked this comment, but check out

http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLo.... Scroll down to "Fixing Double-Checked Locking using Volatile." It's five lines of code without braces, and can be applied without any creativity or reasoning.

You should use enums for singletons i Java. Simple to write, threadsafe and handles serialization. All with guarantees from the Java language specification.
I wasn't actually talking about singletons, just the doublechecked locking pattern. It's also necessary for laziness on objects that are referenced by multiple threads.

I tend to think Singletons are a dubious design pattern, like a lot of other people.