I have a piece of code that I want to run only once for initialization. So far I was using sync.Mutex combined with an if-clause to test if it has been run already. Later I came across the Once type and its DO() function in the same sync package.
The implementation is the following https://golang.org/src/sync/once.go:
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 1 {
return
}
// Slow-path.
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
Looking at the code, it is basically the same thing I've been using before. A mutex combined with an if-clause. However, the added function calls makes this seem rather inefficient to me. I did some testing and tried varous versions:
func test1() {
o.Do(func() {
// Do smth
})
wg.Done()
}
func test2() {
m.Lock()
if !b {
func() {
// Do smth
}()
}
b = true
m.Unlock()
wg.Done()
}
func test3() {
if !b {
m.Lock()
if !b {
func() {
// Do smth
}()
b = true
}
m.Unlock()
}
wg.Done()
}
I tested all versions by running the following code:
wg.Add(10000)
start = time.Now()
for i := 0; i < 10000; i++ {
go testX()
}
wg.Wait()
end = time.Now()
fmt.Printf("elapsed: %v\n", end.Sub(start).Nanoseconds())
with the following resutls:
elapsed: 8002700 //test1
elapsed: 5961600 //test2
elapsed: 5646700 //test3
Is it even worth using the Once type? It is convenient but performance is even worse than test2 which always serializes all routines.
Also, why are they using an atomic int for their if-clause? Storing happens inside the lock anyway.
Edit: Go playground link: https://play.golang.org/p/qlMxPYop7kS NOTICE: this doensn't show the results as time is fixed in the playground.