This article has participated in the third “topic writing” track of the Denver Creators Training Camp. For details, check out: Digg Project | Creators Training Camp third is ongoing, “write” to make a personal impact

This proposition actually has two themes: Go and Memory model

  1. memory modelFor what?
  2. andGoWhat does it matter?

Why raise these two questions?

The first thing you need to know is what the Memory model is for, and every language may have its own practices and specifications for this problem.

Second, what specifications and recommendations does Go make?

Only with these two questions in mind can we better understand the topic of Go Memory Model.

Why do WE need a Memory model

The background of the problem is multi-threaded concurrency. Different programming languages, different compilers (reordering), and different processors (CPU caching) can produce different results than expected when programs run concurrently.

This is the problem the Memory model needs to face and solve.

Instruction rearrangement

In a word: code does not execute in the order you write it

type T struct {
	  value int
}

var done *T
func factory() {
		t := new(T)
		t.value = 13
		done = t
}

func main() {
		go factory()
		for done == nil {}
		print(done.value)
}
Copy the code

Possible scenarios:

  1. Main Goroutine does not detect factory Goroutine writes to Done, g is always nil
  2. Main Goroutine observed that g is not nil and may also print empty MSG flickersThe assignment to g is not complete, but at least g is not nil

First of all, this is a possible situation, not necessarily happen, because the order of the various operations in the program may not be guaranteed, so in various disorderly order, the results of the program will be confused.

This is code reordering, and there is another kind: instruction parallel reordering.

Modern cpus are pipelined, executing multiple instructions at the same time (at different stages of execution, depending on CPU architecture), and if the CPU decides that the instructions in your code are irrelevant, it will execute them in parallel. After parallel execution, the order of the code is further disrupted.

Here’s a concept: happens-before

Happens-before is a concept found in many languages

Happens-before describes the sequence in which two times occur. If you specify a happens-before relationship between operations as the program runs, you can guarantee the order in which the program occurs.

CPU cache

To improve the efficiency of the program, the CPU stores the data in the cache after accessing the data from the internal. The next time it retrieves the data, it retrieves the data directly from the cache.

Of course, cached data will eventually be written to memory, but while the program is running, the data in memory may not be up to date. The latest data may be in the cache.

In modern cpus, there are usually multiple cores, each with its own cache, but only one piece of memory, which is shared. One of the criteria for CPU optimization when reading data from the cache is that the “thread-level” results are correct. That is, when the CPU believes that the current thread (regardless of other threads’ modifications) is consistent with the data in the cache, the cached data will be used.

Of course, if other threads make changes to the shared memory, the CPU does not realize that memory has changed and still fetches data from the cache, resulting in execution errors.

conclusion

To summarize the purpose:

  1. With compiler (instruction reordering) and hardware (different CPU architectures) optimizations, the language needs a specification to clarify the visibility and order of a variable in a concurrent environment
  2. Give developers a concurrency guarantee and provide some access control (some components and libraries) → serialized access

How to limit compiler optimization, instruction parallel optimization, control variable transfer between threads, so that the program calculation results are correct in the case of multi-threaded concurrency

Go Design Advocacy

Don’t communicate by sharing memory, share memory by communicating. — go-sur.github. IO /

If you don’t know this sentence, first Go to Go to see (default we know).

But this was not first said in Go, but in Erlang, a language designed for communication (from which RabbitMQ developed). Explains the Go concurrency model: “Use communication to share memory”.

What does this “correspondence” mean?

  1. Synchronizing messages between different components through communication → resolution behavior
  2. Sharing memory through communication does not lead to contention between threads → resolve contention

competition

Source of competition: concurrent access to shared memory. Go advocates according to the above:

Share the variable → channel, since only one Goroutine can access a channel at a time (there is also a mutex inside); For a channel, producer → channel, the ownership of variables (borrowing the concept of rust) is transferred to the channel, and only one receiver can access the channel at this time. Channel → Consumer: After receiving the MSG, the receiver can do a series of operations according to the message.

How does Go work

A general rule:

  1. Serialized access is achieved using channel operations or other synchronization primitives (primitives in the SYNC/Atomic package)
  2. Data accessed simultaneously by multiple Goroutines must be serialized

Since we’re talking about Goroutine, let’s talk about that

goroutines

Go func() executes, must happens-before this goroutine code executes.

var a string

func f() {
  print(a)
}
func hello() {
  a = "hello, world"
  go f()
}

func main() {
	hello()
}
Copy the code

Describe the entire happens-before implementation:

  1. a = "hello, world"
  2. go f()
  3. F () - > print (a)

So the program can normally output: Hello, world.

channel

Channels play an important role in communication, as mentioned in the previous discussion. Here’s channel’s happens-before principle:

  1. send channel happens before recv channel
  2. close channel happens before read channel
  3. Unbuffered channel,recv channel happens before send channel
  4. Buffered channe, the NTH send happens before the NTH +m send

Here’s an example for 3:

var ch = make(chan int)
var s string

func f() {
  s = "send from f()"
  <-ch
}

func main() {
  go f()
  ch <- struct{}{}
  print(s)
}
Copy the code

You can assume that when ch < -struct {}{} is executed, s is already assigned, so the following s is printable. Go memory model