Gopher refers to north

The Go memory model specifies how one Goroutine can observe writes to the same variable by other Goroutines.

When multiple Goroutines concurrently access the same data, the concurrent access must be serialized. Serialization of reads and writes is guaranteed in Go through channel communication or other synchronization primitives (such as mutex, read and write locks in sync packages, and atomic operations in sync/atomic).

Happens Before

In a single Goroutine, the behavior of reads and writes must be consistent with the execution order specified by the program. In other words, the compiler and processor can reorder instructions in a single Goroutine without changing the behavior defined by the language specification.

a := 1
b := 2
Copy the code

Due to instruction reordering, b := 2 May be executed before a := 1. In a single Goroutine, this adjustment of the execution order does not affect the final result. But multiple Goroutine scenarios can cause problems.

var a, b int
// goroutine A
go func(a) {
    a := 5
    b := 1} ()// goroutine B
go func(a) {
    for b == 1 {}
    fmt.Println(a)
}()
Copy the code

When executing the above code, goroutine B is expected to print 5 normally, but because of instruction reordering, B := 1 May be executed before A := 5, and goroutine B may eventually print 0.

Note: The above example is an incorrect example and is for illustration only.

To clarify the requirements for read and write operations, Go introduces happens-before, which represents a partial order in which memory operations are performed.

The role of the happens-before

When multiple Goroutines access a shared variable, they must establish synchronous events to ensure happens-before conditions to ensure that the read can observe the expected write.

What Happens Before

If event E1 occurs before event e2, then we say that e2 occurs after E1. Similarly, if E1 doesn’t happen before E2 and doesn’t happen after E2, then we say e1 and E2 happen at the same time.

In a single Goroutine, the happens-before order is the order in which the program is executed. So what’s the order of happens-before? Let’s look at the following conditions.

R allows w to be observed only if the following two conditions are met for the read operation r and write operation W of a variable v:

  1. R doesn’t happen before W.
  2. No other writes occur after w and before r.

To ensure that a read operation R of variable V can observe a particular write operation W, we need to ensure that W is the only write operation allowed to be observed by R. Then, r ensures that W is observed if both r and W satisfy the following conditions:

  1. W comes before R.
  2. Other writes occur before w and after r.

There is no concurrency in a single Goroutine; the two conditions are equivalent. On this basis, Lao Xu extends that the two conditions are equivalent for a single core operating environment. In the case of concurrency, the latter set of conditions is more stringent than the first.

If you’re confused, you’re right! Old Xu was also very confused at the beginning, these two groups of conditions are the same ah. For this reason, Xu made repeated comparisons with the original text to ensure that the above understanding is correct.

Let’s switch gears and work backwards. If these two groups of conditions are the same, there is no need to write the original text twice.

Before continuing the analysis, I would like to thank my Chinese teacher, without you I would not have found the difference.

If r does not occur before W, r can occur after w or at the same time as W, as shown in the figure below (solid means simultaneous).

If no other writes occur after w and before r, other writes w’ may occur before w or at the same time as w, or after r or at the same time as r, as shown below (solid means simultaneous).

The second set of conditions is clear: w occurs before r and other writes can only occur before w or after r, as shown below (hollow means not simultaneous).

Now you can see why the second set of conditions is more stringent than the first set of conditions. In the first group, w is allowed to be observed, and in the second group, W is guaranteed to be observed.

Synchronization in Go

Here are some of the synchronization events that are agreed upon in Go to ensure that the program follows the happens-before principle so that concurrent goroutines are relatively orderly.

Initialization of Go

Program initialization runs in a single Goroutine, but that Goroutine can create other goroutines that run concurrently.

If package P imports package q, the q package init function ends before the p package init function ends. Execution of main occurs after all init functions have been executed.

Goroutine creation is complete

The creation of a Goroutine precedes execution of a Goroutine. Hsu thought this was mostly nonsense, but it wasn’t always that simple, and the implication was that goroutine creation blocked.

func sleep(a) bool {
   time.Sleep(time.Second)
   return true
}

go fmt.Println(sleep())
Copy the code

The above code blocks the main goroutine for a second before the child goroutine is created.

Goroutine’s exit was impossible to predict. If one goroutine is used to observe another goroutine, use locks or channels to ensure relative order.

Sending and receiving of a Channel

Channel communication is the primary means of synchronization between Goroutines.

  • The send action of a Channel precedes the completion of the corresponding receive action.

  • The acceptance of an unbuffered Channel precedes the completion of a send on that Channel.

These two points can be summed up as four actions: start to send, start to receive, finish to send and finish to receive, and their timing relationship is as follows.

Start Send > Accept complete Start Accept > Send completeCopy the code

Note: there is no clear sequence between the start of sending and the start of receiving

  • The closing of a Channel occurs before a zero value acceptance is returned due to Channel closure.

  • The KTH acceptance of a Channel of capacity C precedes the completion of the k+C transmission on that Channel.

Limit method should be easier to understand. If C is 0 and k is 1, the meaning is the same as that of unbuffered Channel.

Lock

For any sync.mutex or sync.rwmutex variable l and n < m, the NTH call to l.lock () is returned before the MTH call to l.lock ().

Assuming n is 1 and m is 2, the second call to l.lock () must be called before it returns.

There is such an n for the variable L of sync.rwmutex that the return of the call to l.lock () occurs after the NTH l.lock (), and the matching l.unlock () occurs before the NTH + 1 l.lock ().

I have to say, the above sentence is almost incomprehensible. Lao Xu translated it into adult words:

With write locks: the call return of l.lock () occurs after l.lock ().

With read locks: the call to l.unlock () occurs before l.lock ().

Note: Invoking l.lock () without calling l.lock () and l.lock () without calling l.lock () causes panic.

Once

The return of f in once.Do(f) precedes the return of any other once.

Incorrect synchronization

Mistake # 1

var a, b int

func f(a) {
	a = 1
	b = 2
}

func g(a) {
	print(b)
	print(a)
}

func main(a) {
	go f()
	g()
}
Copy the code

This example seems simple, but Xu believes that most people will ignore the abnormal output caused by reordering instructions. If b=2 occurs before A =1 after the goroutine f instruction is reordered, then the main Goroutine sees b change but not A change, so it is possible to print 20.

Hsu has done a lot of experiments in the local area and the result is always 0, 20 which is probably a theoretical output.

Mistake # 2

var a string
var done bool

func setup(a) {
	a = "hello, world"
	done = true
}

func doprint(a) {
	if! done { once.Do(setup) }print(a)
}

func twoprint(a) {
	go doprint()
	go doprint()
}
Copy the code

This double detection is intended to avoid synchronization overhead, but it is still possible to print an empty string instead of “hello, world”. To be honest, Xu himself could not guarantee that he had not written such code before. The only scenario that can be thought of is when one of the Goroutine doprint executes done=true (reordering causes done=true to be executed before a=”hello, world”), The other Goroutine doprint just starts executing and observes that done has a value of true to print an empty string.

Finally, I sincerely hope that this article can be of some help to all readers. Of course, if you find any mistakes, please contact Lao Xu to correct them.

reference

golang.org/ref/mem