gosynchronizationrace-conditiondouble-checked-locking

How is this code snippet an example of incorrect synchronization?


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 to a. 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{}
}

Solution

  • 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:

    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"
    }
    

    (Playground)

    This may be enough to convince you that the program indeed contains a synchronization bug.