How is this code snippet an example of incorrect synchronization?

134 views Asked by At

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{}
}
3

There are 3 answers

1
jub0bs On BEST ANSWER

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:

  • One doprint goroutine doesn't observe the write to done and therefore initiates a single execution of the setup function;
  • The other 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"
}

(Playground)

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

2
Burak Serdar On

According to the Go memory model:

https://golang.org/ref/mem

There are no guarantees that one goroutine will see the operations performed by another goroutine unless there is an explicit synchronization between the two using channels, mutex. etc.

In your example: the fact that a goroutines sees done=true does not imply it will see a set. This is only guaranteed if there is explicit synchronization between the goroutines.

The sync.Once probably offers such synchronization, so that's why you have not observed this behavior. There is still a memory race, and on a different platform with a different implementation of sync.Once, things may change.

0
Hymns For Disco On

The program is not memory safe because:

  • Multiple goroutines concurrently access the same memory (done and a).
  • The concurrent access is not always controlled by explicit synchronization.
  • The accesses may write to / modify the memory.

Trying to reason about how the program will or will not behave with regard to these variables is probably just unneeded confusion, because it's literally undefined behaviour. There is no "correct" answer. Only circumstantial observations, which have no hard guarantee of if or when they hold true.