# 同步原语与锁 当提到并发编程、多线程编程时,我们往往都离不开『锁』这一概念,Go 语言作为一个原生支持用户态进程 Goroutine 的语言,也一定会为开发者提供这一功能,锁的主要作用就是保证多个线程或者 Goroutine 在访问同一片内存时不会出现混乱的问题,锁其实是一种并发编程中的同步原语(Synchronization Primitives)。 在这一节中我们就会介绍 Go 语言中常见的同步原语 `Mutex`、`RWMutex`、`WaitGroup`、`Once` 和 `Cond` 以及扩展原语 `ErrGroup`、`Semaphore`和 `SingleFlight` 的实现原理,同时也会涉及互斥锁、信号量等并发编程中的常见概念。 ## 1. 基本原语 Go 语言在 [sync](https://golang.org/pkg/sync/) 包中提供了用于同步的一些基本原语,包括常见的互斥锁 `Mutex` 与读写互斥锁 `RWMutex` 以及 `Once`、`WaitGroup`。 ![golang-basic-sync-primitives](https://img.draveness.me/golang-basic-sync-primitives.png) 这些基本原语的主要作用是提供较为基础的同步功能,我们应该使用 Channel 和通信来实现更加高级的同步机制,我们在这一节中并不会介绍标准库中全部的原语,而是会介绍其中比较常见的 `Mutex`、`RWMutex`、`Once`、`WaitGroup` 和 `Cond`,我们并不会涉及剩下两个用于存取数据的结构体 `Map` 和 `Pool` ### 1.1. Mutex Go 语言中的互斥锁在 `sync` 包中,它由两个字段 `state` 和 `sema` 组成,`state` 表示当前互斥锁的状态,而 `sema` 真正用于控制锁状态的信号量,这两个加起来只占 8 个字节空间的结构体就表示了 Go 语言中的互斥锁。 ```go type Mutex struct { state int32 sema uint32 } ``` #### 状态 互斥锁的状态是用 `int32` 来表示的,但是锁的`state`并不是互斥的,它的最低三位分别表示 `mutexLocked`、`mutexWoken` 和 `mutexStarving`,剩下的位置都用来表示当前有多少个 Goroutine 等待互斥锁被释放: ![golang-mutex-state](https://img.draveness.me/golang-mutex-state.png) ``` |-----------------|---------------| -----------| -----------| | waitGoroutines | mutexStarving | mutexWoken | mutexLocked| ``` **互斥锁在被创建出来时,所有的状态位的默认值都是 `0`,当互斥锁被锁定时 `mutexLocked` 就会被置成 `1`、当互斥锁被在正常模式下被唤醒时 `mutexWoken` 就会被被置成 `1`、`mutexStarving` 用于表示当前的互斥锁进入了状态,最后的几位是在当前互斥锁上等待的 Goroutine 个数**。 #### 饥饿模式 `Mutex` 在可能会进入饥饿模式,饥饿模式主要功能就是保证互斥锁的获取的『公平性』(Fairness)。 互斥锁可以同时处于两种不同的模式,也就是正常模式和饥饿模式,在正常模式下,所有锁的等待者都会按照先进先出的顺序获取锁,但是如果一个刚刚被唤起的 Goroutine 遇到了新的 Goroutine 进程也调用了 `Lock` 方法时,大概率会获取不到锁,为了减少这种情况的出现,防止 Goroutine 被『饿死』,一旦 Goroutine 超过 1ms 没有获取到锁,它就会将当前互斥锁切换饥饿模式。 ![golang-mutex-mode](https://img.draveness.me/golang-mutex-mode.png) 在饥饿模式中,互斥锁会被直接交给等待队列最前面的 Goroutine,新的 Goroutine 在这时不能获取锁、也不会进入自旋的状态,它们只会在队列的末尾等待,如果一个 Goroutine 获得了互斥锁并且它是队列中最末尾的协程或者它等待的时间少于 1ms,那么当前的互斥锁就会被切换回正常模式。 相比于饥饿模式,正常模式下的互斥锁能够提供更好地性能,饥饿模式的主要作用就是避免一些 Goroutine 由于陷入等待无法获取锁而造成较高的尾延时,这也是对 `Mutex` 的一个优化。 #### 加锁 互斥锁 `Mutex` 的加锁是靠 `Lock` 方法完成的,最新的 Go 语言源代码中已经将 `Lock` 方法进行了简化,方法的主干只保留了最常见、简单并且快速的情况;当锁的状态是 `0` 时直接将 `mutexLocked` 位置成 `1`: ```go func (m *Mutex) Lock() { if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { return } m.lockSlow() } ```