Home >Backend Development >Golang >Introduction to the memory model of Go language
Go's memory model details the conditions under which "a read operation on a variable in one goroutine can detect a write operation on the variable in another goroutine".
Happens Before
For a goroutine, the read and write operations of its variables must perform as expected from the code written. are consistent. In other words, without changing the performance of the program, the compiler and processor may change the order of operations of variables in order to optimize the code: instructions are rearranged out of order.
But when two different goroutines operate on the same variable, different goroutines may have inconsistent understanding of the order of variable operations due to instruction rearrangement. For example, if one goroutine executes a = 1; b = 2;, another goroutine may notice that variable b is changed before variable a.
In order to solve this ambiguity problem, the Go language introduces the concept of happens before, which is used to describe the order of memory operations. If event e1 happens before event e2, we say event e2 happens after e1.
If event e1 does not happen before event e2, and does not happen after e2, we say that events e1 and e2 occur at the same time.
For a single goroutine, the order of happens before is consistent with the order of code.
If the following conditions are met, a "read event r" for variable v can perceive another "write event w" for variable v:
1. "Write event w" ” happens before “read event r”.
2. There is no writing event w to variable v that satisfies happens after w and happens before r at the same time.
In order to ensure that the read event r can sense the write event to the variable v, we must first ensure that w is the only write event for the variable v. At the same time, the following conditions must be met:
1. "Write event w" happens before "read event r".
2. Other accesses to variable v must happen before "write event w" or happen after "read event r".
The second set of conditions is more stringent than the first set of conditions. Because it requires that there can be no other read operations in the program where w and r are executed in parallel.
For the two sets of conditions to be equivalent in a single goroutine, the read event can ensure that the write event to the variable is sensed. However, for shared variable v between two goroutines, we must guarantee the happens-before condition through synchronization events (this is a necessary condition for read events to be aware of write events).
Automatically initializing variable v to zero also belongs to this memory operation model.
The order of reading and writing data that exceeds the length of one machine word is not guaranteed.
Synchronization
Initialization
The program is initialized in a separate goroutine implement. Goroutines created during initialization will be started after the first goroutine used to initialize has completed execution.
If package p imports package q, the init initialization function of package q will be executed before the initialization of package p.
The entry function main.main of the program is started after all init functions are executed.
Newly created goroutines in any init function will be executed after all init functions are completed.
Creation of Goroutine
The go statement used to start the goroutine runs before the goroutine.
For example, the following program:
var a string; func f() { print(a); } func hello() { a = "hello, world"; go f(); }
Calling the hello function will print "hello, world" at a certain moment (possibly after the hello function returns).
Channel communication Pipeline communication
Using pipe communication is the main method of synchronization between two goroutines. Common usage is that different goroutines perform read and write operations on the same pipe, one goroutines writes to the pipe, and the other goroutines reads data from the pipe.
The sending operation on the pipe occurs before the receiving of the pipe is completed (happens before).
For example, this program:
var c = make(chan int, 10) var a string func f() { a = "hello, world"; c <- 0; } func main() { go f(); <-c; print(a); }
can ensure that "hello, world" will be output. Because the assignment of a occurs before sending data to pipe c, and the sending operation of the pipe occurs before the pipe reception is completed. Therefore, at the time of printing, a has been assigned a value.
Receiving data from an unbuffered pipe is sent before sending data to the pipe is completed.
The following is a sample program:
var c = make(chan int) var a string func f() { a = "hello, world"; <-c; } func main() { go f(); c <- 0; print(a); }
It can also ensure the output of "hello, world". Because the assignment of a occurs before receiving data from the pipe, and the operation of receiving data from the pipe occurs before sending to the unbuffered pipe is completed. Therefore, when printing, a has been assigned.
If you use a buffered pipe (such as c = make(chan int, 1)), there is no guarantee that the "hello, world" result will be output (it may be an empty string, but it will definitely not be unknown) string, or cause the program to crash).
Lock
Package sync implements two types of locks: sync.Mutex and sync.RWMutex.
对于任意 sync.Mutex 或 sync.RWMutex 变量l。 如果 n < m ,那么第n次 l.Unlock() 调用在第 m次 l.Lock()调用返回前发生。
例如程序:
var l sync.Mutex var a string func f() { a = "hello, world"; l.Unlock(); } func main() { l.Lock(); go f(); l.Lock(); print(a); }
可以确保输出“hello, world”结果。因为,第一次 l.Unlock() 调用(在f函数中)在第二次 l.Lock() 调用(在main 函数中)返回之前发生,也就是在 print 函数调用之前发生。
For any call to l.RLock on a sync.RWMutex variable l, there is an n such that the l.RLock happens (returns) after the n'th call to l.Unlock and the matching l.RUnlock happens before the n+1'th call to l.Lock.
Once
包once提供了一个在多个goroutines中进行初始化的方法。多个goroutines可以 通过 once.Do(f) 方式调用f函数。但是,f函数 只会被执行一次,其他的调用将被阻塞直到唯一执行的f()返回。once.Do(f) 中唯一执行的f()发生在所有的 once.Do(f) 返回之前。
有代码:
var a string func setup() { a = "hello, world"; } func doprint() { once.Do(setup); print(a); } func twoprint() { go doprint(); go doprint(); }
调用twoprint会输出“hello, world”两次。第一次twoprint 函数会运行setup唯一一次。
错误的同步方式
注意:变量读操作虽然可以侦测到变量的写操作,但是并不能保证对变量的读操作就一定发生在写操作之后。
例如:
var a, b int func f() { a = 1; b = 2; } func g() { print(b); print(a); } func main() { go f(); g(); }
函数g可能输出2,也可能输出0。
这种情形使得我们必须回避一些看似合理的用法。
这里用Double-checked locking的方法来代替同步。在例子中,twoprint函数可能得到错误的值:
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(); }
在doprint函数中,写done暗示已经给a赋值了,但是没有办法给出保证这一点,所以函数可能输出空的值。
另一个错误陷阱是忙等待:
var a string var done bool func setup() { a = "hello, world"; done = true; } func main() { go setup(); for !done { } print(a); }
我们没有办法保证在main中看到了done值被修改的同时也 能看到a被修改,因此程序可能输出空字符串。更坏的结果是,main 函数可能永远不知道done被修改,因为在两个线程之间没有同步操作,这样main 函数永远不能返回。
下面的用法本质上也是同样的问题.
type T struct { msg string; } var g *T func setup() { t := new(T); t.msg = "hello, world"; g = t; } func main() { go setup(); for g == nil { } print(g.msg); }
即使main观察到了 g != nil 条件并且退出了循环,但是任何然 不能保证它看到了g.msg的初始化之后的结果。
更多go语言知识请关注PHP中文网go语言教程栏目。
The above is the detailed content of Introduction to the memory model of Go language. For more information, please follow other related articles on the PHP Chinese website!