浅析并发模型:共享内存/Actor/CSP
Golang 编程中,涉及到并发问题时,通常有以下两种解决方案:
- 采用共享内存模型,利用 sync.Mutex / sync.RWMutex 等加锁、设置临界区解决数据并发访问问题;
- 采用消息通信模型,利用 channel 进行 goroutine 间通信,避开内存共享来解决。
官方推荐大家采用第二种方案,那么它究竟好在哪里呢?
共享内存模型
所谓共享内存模型,就是我们在并发编程的时候,通过让多个并发执行实体(线程/Go程/协程/…)去操作同一个共享变量,从而达到通信的目的。
比如下面这个 Go 程序例子,全局变量 count 初始值 10000,然后开启 10000 个 Goroutine 去分别执行一次取 count 并 -1 的操作。
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)
}
你认为执行完毕后 count 还剩多少?0 ?还是?
答案是:不确定
可以列举几次的运行结果:
1221
1270
1259
...
没有一次等于 0。为什么?原因很简单,学过并发编程的朋友都会回答到:数据同步问题。
很好理解为什么会出现问题,在扣减 count 的过程中,需要先获取共享的 count 值,执行 -1 后赋值回去,假如此时的 count == 9000,GoroutineA 和 GoroutineB 同时读到 count == 9000(并发过程中这是完全可能的),然后各自 -1,赋值回 count,结果怎么样,虽然两个 Goroutine 均扣减了 count,但是最终赋值回去的都是 8999,这就叫做数据不同步,所谓数据同步,就是协同步调,按照顺序来执行,明显上面这个过程就违背了,因此出现了并发问题。
在共享内存模型下解决这一问题的基本方式就是通过锁机制实现资源的互斥访问,将需要互斥访问的资源(也就是共享的资源,一般称为临界资源/互斥资源)加锁,当一个 Goroutine 访问临界资源时,如果未加锁,则加锁访问,访问结束后释放锁,其它 Goroutine 访问加锁资源时会阻塞住,通过这种方式可以实现数据的同步问题,其中代码中涉及到了操作临界资源的代码段叫临界区。
现在把上面的程序进行改造,将 count 设置为临界资源,通过 golang 提供的 sync.Mutex 互斥量加锁:
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)
}
通过实现 count 的互斥访问,此时 10000 个 Goroutine 并发扣减 count 不再出现由于数据不同步而扣减失败的现象,因此执行结果输出为 0
,不多不少,全部扣减完成。
共享内存模型必须要考虑的是共享变量的同步问题,当共享变量变多的时候,并发编程中需要考虑的数据同步问题就会越来越多,这也共享内存容易出错的原因所在。
在所有的并发模型中,共享内存这种方案最为常见,也是较为底层的一种实现方案,这种方案源于早期的单核时代,仅从单机角度去解决并发问题的话,这个并发模型的设计是无可厚非的。但是在分布式时代、多核时代,一旦出现并行执行的问题,共享内存模型就有些捉襟见肘了,因此针对这些场景,需要有更好的方案来解决,首先说方案是多种多样的,比如可以从锁机制本身去考虑,因此有分布式锁的实现方式,本文因为主要是将的并发模型,因此接下来就介绍一种更适合分布式时代及多核时代的并发模型。
消息传递模型
消息传递模型,先不去管接下来要说的两种设计实现,首先要记住这句话:
不要通过共享内存来通信,而应该通过通信来共享内存
很多人在接触到消息传递模型这种并发模型的时候都会听到这句话,我在学习 golang 的时候不知道听到多少次这句话了,但是一开始还是有些不理解的,感觉说的有点绕,但是随着接触到的并发编程相关的代码越来越多,才逐渐明白这句话的意义。