I am trying to understand the example with incorrect sync code from The Go Memory Model.
Double-checked locking is an attempt to avoid the overhead of synchronization. For example, the twoprint program might be incorrectly written as:
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func doprint() {
if !done {
once.Do(setup)
}
print(a)
}
func twoprint() {
go doprint()
go doprint()
}
but there is no guarantee that, in
doprint
, observing the write to done implies observing the write toa
. This version can (incorrectly) print an empty string instead of"hello, world"
.
What are the detailed reasons for an empty string printed in place of "hello world"? I ran this code about five times, and every time, it printed "hello world".
Would the compiler swap a line a = "hello, world"
and done = true
for optimization? Only in this case, I can understand why an empty string would be printed.
Thanks a lot! At the bottom, I've attached the changed code for the test.
package main
import(
"fmt"
"sync"
)
var a string
var done bool
var on sync.Once
func setup() {
a = "hello, world"
done = true
}
func doprint() {
if !done {
on.Do(setup)
}
fmt.Println(a)
}
func main() {
go doprint()
go doprint()
select{}
}
The reference page about the Go Memory Model tells you the following:
compilers and processors may reorder the reads and writes executed within a single goroutine only when the reordering does not change the behavior within that goroutine as defined by the language specification.
The compiler may therefore reorder the two writes inside the body of the setup
function, from
a = "hello, world"
done = true
to
done = true
a = "hello, world"
The following situation may then occur:
doprint
goroutine doesn't observe the write to done
and therefore initiates a single execution of the setup
function;doPrint
goroutine observes the write to done
but finishes executing before observing the write to a
; it therefore prints the zero value of a
's type, i.e. the empty string.I ran this code about five times, and every time, it printed "hello world".
You need to understand the distinction between a synchronization bug (a property of the code) and a race condition (a property of a particular execution); this post by Valentin Deleplace does a great job at elucidating that distinction. In short, a synchronization bug may or may not give rise to a race condition; however, just because a race condition doesn't manifest itself in a number of executions of your program doesn't mean your program is bug-free.
Here, you can "force" the race condition to occur simply by reordering the two writes in setup
and adding a tiny sleep between the two.
func setup() {
done = true
time.Sleep(1 * time.Millisecond)
a = "hello, world"
}
This may be enough to convince you that the program indeed contains a synchronization bug.