In Golang programming, when it comes to concurrency problems, there are usually two solutions.

  • Adopt the shared memory model and use sync.Mutex / sync.RWMutex etc. to add locks and set critical zones to solve the data concurrent access problem.
  • Adopt the message communication model and use channel for inter-goroutine communication to avoid memory sharing to solve the problem.

The official recommendation is to use the second option, so what is it good for?

Shared Memory Model

The shared memory model is a way to communicate with multiple concurrently executing entities (threads/Goroutines…) to manipulate the same shared variable during concurrent programming.

For example, in the following Go program example, the global variable count starts at 10000, and then opens 10000 Goroutines to perform the operation of taking count and -1 once each.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
    "fmt"
    "sync"
)

var (
    count = 10000
    wg    sync.WaitGroup
)

func buy() {
    defer wg.Done()
    countReplica := count
    count = countReplica - 1
}

func main() {
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go buy()
    }
    wg.Wait()
    fmt.Println(count)
}

How much count do you think will be left after the execution is completed?

The answer is: uncertain

You can list the results of several runs.

1
2
3
4
1221
1270
1259
...

Why? The reason is very simple, and those who have learned concurrent programming will answer to it: Data synchronization problem.

It is easy to understand why there is a problem. In the process of subtracting count, you need to get the shared count value first, execute -1 and assign it back, if the count == 9000, GoroutineA and GoroutineB read count == 9000 at the same time (which is perfectly possible in a concurrent process). Then each of them -1 and assign back count, what happens? Although both Goroutines have deducted count, the final assignment is 8999, which is called data desynchronization. The so-called data synchronization, is to coordinate the pace, in order to execute. Obviously the above process violates this, so there is a concurrency problem.

The basic way to solve this problem under the shared memory model is to achieve mutually exclusive access to resources through the lock mechanism, the resources that need mutually exclusive access (that is, shared resources, generally known as critical resources/mutually exclusive resources) are locked, when a Goroutine accesses a critical resource, if it is not locked, it is accessed with a lock, and the lock is released when the access is finished, and other Goroutines In this way, the synchronization of data can be achieved, where the code involved in the operation of the critical resources is called the critical area.

Now the above program is modified to set count as a critical resource and lock it with the sync.Mutex mutex provided by golang.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
    "fmt"
    "sync"
)

var (
    count = 10000
    wg    sync.WaitGroup
    mutex sync.Mutex
)


func buyWithMutex() {
    mutex.Lock()
    defer wg.Done()
    //---------- 临界区 --------------
    countReplica := count
    count = countReplica - 1
    //-------------------------------
    mutex.Unlock()
}

func main() {
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go buyWithMutex()
    }
    wg.Wait()
    fmt.Println(count)
}

By implementing a mutually exclusive access to the count, the 10,000 Goroutine concurrent deduction count will no longer fail due to the data being out of sync. So the result is 0, no more, no less, and the deduction is complete.

One thing that must be considered in the shared memory model is the synchronization of shared variables. When there are more shared variables, there are more and more data synchronization issues to consider in concurrent programming. This is where shared memory becomes error-prone.

Among all concurrency models, shared memory is the most common and the lowest level of implementation, which originated from the early single-core era. However, in the distributed and multi-core era, once the problem of parallel execution arises, the shared memory model is somewhat stretched. Therefore, a better solution is needed for these scenarios. For example, we can consider the locking mechanism itself, so there are distributed locking implementations. In this paper, since we mainly focus on the concurrency model, we will introduce a concurrency model that is more suitable for the distributed era and the multi-core era.

Messaging Model

The message-passing model, leaving aside the two design implementations to be described next, begins with this statement in mind.

Don’t communicate by sharing memory, but by communicating to share memory

I don’t know how many times I heard this phrase when I was learning golang, but at first I still didn’t understand it, I felt it was a bit roundabout, but as I came across more and more concurrent programming-related code, I gradually understood the meaning of this phrase.