Happens before is a common concept in the memory model, and Go also defines happens before and the various operations that occur happens before relationships, because of these happens before operation Guarantees, We write a multi-goroutine program that works the way we expect it to.
What is happens before relationship
Happens before defines a partial-order relationship between two operations with transitivity. For two operations E1 and E2:
- If E1 happens before E2, then E2 happens after E1;
- If E1 happens E2, E2 happens before E3, then E1 happens E3;
- If E1 and E2 do not have any happens before relationships, say E1 and E2 happen concurrently.
The role of happens before
Happens before is primarily used to ensure the visibility of memory operations. If you want to ensure that E1 memory writes can be read by E2, then you need to meet the following:
- E1 happens before E2;
- All other writes for this memory, either happens before E1, or happens after E2. That is, there can be no other write operation E3, this E3 happens concurrently e1/e2.
Why do you need to define happens before relationships to ensure the visibility of memory operations? The reason is that there is no limit to the fact that the various optimizations that the compiler and CPU use can have an impact on this, specifically the operation reordering and CPU Cacheline cache synchronization:
- Operation reordering. Modern CPUs are typically pipelined and have multiple cores so that multiple instructions can be executed simultaneously. However, there are times when an instruction needs to wait for the result of a previous instruction, or other circumstances that cause the execution of the instruction to be delayed. This is the time to execute the next ready instruction to use the CPU as efficiently as possible. Action reordering can occur in two stages:
- Compiler instruction reordering
- CPU Disorderly Execution
- The synchronization problem of the CPU multi-core independent cache line. Multicore CPUs often have their own first-level caches and level two caches, accessing cached data quickly. However, if the cache is not synchronized to main memory and other core caches, the other core read caches will read the stale data.
For example, look at a multi-goroutine program:
// Sample Routine 1funchappensBeforeMulti(iint){i+=2// E1gofunc(){// G1 goroutine createfmt.Println(i)// E2}()// G2 goroutine destryo}
To explain this:
- If the compiler or CPU is reordered, then the E1 instruction may be executed after E2, thereby outputting the wrong value;
- The variable i is cached by the CPU to cache line, E1 changes to I only rewrite the cache line, not write back to main memory, and E2 in the other goroutine execution, if and E1 is not on the same core, then E2 output is the wrong value.
and happens before relationship, is the compiler and CPU restrictions, prohibit the violation of happens before relationship command reordering and disorderly execution behavior, as well as the necessary circumstances to ensure cacheline data updates.
The happens before guarantee defined in Go
1) Single Thread
- In a single-threaded environment, all expressions, according to the order in the code, have happens before relationships.
CPU and correctly implemented compiler, for single-threaded case of happens before relationship, are guaranteed. This is not to say that the compiler or CPU does not reorder, as long as the optimization does not affect the happens before relationship is possible. This is based on analyzing the dependency of the data, and the operations that are not dependent on the data can be reordered.
such as the following programs:
// Sample Routine 2funchappsBefore(iint,jint){i+=2 // E1j+=10 // E2fmt.Println(i+j)//E3}
The order of execution between E1 and E2 is not related, as long as the E3 is guaranteed not to be executed before E1 and E2.
2) Init function
- If package P2 is imported in package P1, the init function in P2 happens before all operations in P1
- The main function happens after all the init functions
3) Goroutine
- Goroutine creation happens before all actions in this Goroutine
- Goroutine destroy happens after all operations in this Goroutine
We mentioned above the sample Routine 1, according to Rule 1, E1 happens before G1, in accordance with these rules, G1 happens before E2, thereby E1 happens before E2.
4) Channel
- Send operation on an element happens before corresponding receive completion operation
- Close operation on channel happens receive shutdown notification action on before receive side
- For the unbuffered Channel, the receive operation for an element happens before corresponding send completion operation
- For the buffered channel, assuming that the channel's buffer size is C, then the receive operation on the K element, happens before K+c Send, completes the operation. you can see that the last unbuffered channel rule is the exception to this rule c=0.
First notice here that the send and send finishes, which are two events, receive and receive completion are also two events.
Then, the Buffered channel here has a pit, its happens before guaranteed to be weaker than unbuffered, this weak only in "write before receive, read after send" in this case there is a problem. and "write before send, read after receive", so the use is not a problem, this is the usual mode of writing programs we often do not mistake here!
//Channel routine 1var C = Make(Chan int)var a stringfunc F() {a = "Hello, World"<-C}func Main() {Go F()C <- 0Print(a)}
//Channel routine 2var C = Make(Chan int, Ten)var a stringfunc F() {a = "Hello, World"<-C}func Main() {Go F()C <- 0Print(a)}
//Channel routine 3var C = Make(Chan int, Ten)var a stringfunc F() {a = "Hello, World"C <- 0}func Main() {Go F()<-CPrint(a)}
For example, the above three programs, using channel to do synchronization, program 1 and Program 3 is able to guarantee happens before relationship, program 2 is not able, that is, the program may not output "Hello, world" as expected.
5) Lock
There are mutex and rwmutex two kinds of locks in go, rwmutex in addition to supporting mutually exclusive lock/unlock, also support shared rlock/runlock.
- For a Mutex/rwmutex, set n < m, the nth unlock operation happens before the first m lock operation.
- For a Rwmutex, there is a value n,rlock operation happens after nth unlock, which corresponds to Runlockhappens before of N+1 lock operation.
The simple understanding is that this time lock always happens after the last unlock, read-write lock Rlock happensafter last unlock, its corresponding runlock happens before next lock.
6) Once
Once. Do, happens before any one of the once. The return of the Do call.
If you have a good understanding of the memory model of the JVM and the happens before relationship of the definitions, it is very similar to how the memory model of Go is explained, and it is very easy to understand. There is nothing new in the sun to understand the memory model design of a language, and other similar languages can be easily understood. If it's a front-end or a programmer using node, then you don't need to know that, after all, there's always only one thread running.