一、概述
并发原语是在并发编程中用于控制和协调多个并发执行单元(如线程、进程、协程 )之间操作的基本操作单元或机制,能确保并发程序正确、高效运行,避免数据竞争、死锁等问题 。golang 常见的并发原语如下:
- 锁(Mutex): sync.Mutex
- 读写锁(RWMutex): sync.RWMutex
- 原子操作(Atomic): sync/atomic
- 信号量(Semaphore): 使用channel实现
- 条件变量(Condition Variable): sync.Cond配合互斥锁
- 通道(Channel): channel
今天,我们将针对 golang 中的常用的 sync.Mutex 展开讲解。
二、sync.Mutex原理浅析
2.1 核心数据结构
golang
type Mutex struct {
state int32 // 状态字段
sema uint32 // 信号量
}
- sema: 是一个信号量,用于实现 goroutine 的阻塞与唤醒操作。
- state: 是一个 32 位整数,用于表示锁的状态。不同的位有着不同的含义,涵盖锁是否被持有、是否有等待者、是否处于饥饿模式等。
- 第 0 位(mutexLocked): 锁定位,表示锁是否被持有。
- 1: 代表锁已被持有状态;
- 0: 代表锁处于未被持有状态;
- 第 1 位( mutexWoken): 唤醒位,表示是否有被唤醒的协程(用于避免重复唤醒)。
- 1: 代表有 goroutine 已被唤醒,正在尝试获取锁;
- 第 2 位(mutexStarving): 饥饿位,表示当前是否处于饥饿模式。
- 1: 代表锁处于饥饿模式;
- 0: 代表锁处于正常模式;
- 剩余 29 位: 等待者计数,表示记录等待锁的协程数量(通过信号量排队)。
- 第 0 位(mutexLocked): 锁定位,表示锁是否被持有。
2.2 操作模式
互斥锁可以处于两种操作模式:正常模式和饥饿模式。正常模式 具有相当好的性能,因为即使存在处于阻塞状态的等待协程,一个协程也可以连续多次获取到互斥锁。而饥饿模式对于防止出现尾部延迟的极端情况非常重要。
2.2.1 正常模式
- 在正常模式下,等待获取锁的 goroutine 会按照先进先出(FIFO)的顺序排队。
- 被唤醒的等待者并不直接拥有锁,而是要和新到来的 goroutine 竞争锁的所有权。新到来的 goroutine 因已经在 CPU 上运行,所以具有一定优势,被唤醒的等待者可能竞争失败,此时它会被重新排到等待队列的队首。
- 若一个等待者在超过 1 毫秒的时间内都未能获取到锁,锁就会切换到饥饿模式。
2.2.2 饥饿模式
- 在饥饿模式下,锁的所有权会直接从解锁的 goroutine 传递给等待队列队首的 goroutine。
- 新到来的 goroutine 即便发现锁看似处于解锁状态,也不会尝试获取锁,而是直接排到等待队列的队尾。
- 当一个等待者获得锁后,若它是等待队列中的最后一个等待者,或者其等待时间少于 1 毫秒,锁就会切换回正常模式。
2.2.3 模式切换
如果一个等待协程获得了互斥锁的所有权,并且发现以下两种情况之一:
- 它是等待队列中的最后一个协程;
- 它的等待时间少于 1 毫秒,那么互斥锁会切换回正常操作模式。
2.3 操作流程
2.3.1 加锁(Lock())
- 快速路径: 若锁未被持有(
锁定位=0
),直接通过CAS获取; - 慢路径:
- 若可自旋(多核、正常模式、尝试次数未超限),则自旋等待锁释放。
- 自旋失败后,进入等待队列并递增等待计数。
- 若等待时间过长,则触发饥饿模式。
2.3.2 解锁(Unlock())
- 快速路径: :清除
mutexLocked
位,若此时无等待协程,直接返回。 - 慢路径: :根据当前模式唤醒协程。
- 正常模式: 唤醒队列头部协程,与新协程公平竞争。
- 饥饿模式: 直接将锁交给队列头部协程,新协程不参加自选直接进入等待队列。
三、代码示例
golang
package main
import (
"fmt"
"sync"
)
var (
counter int
mutex sync.Mutex
)
func increment() {
mutex.Lock()
defer mutex.Unlock()
counter++
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
// 期望输出:Counter: 1000
fmt.Println("Counter:", counter)
}
示例解释说明: 在上述代码中,increment
函数借助 mutex.Lock()
和 mutex.Unlock()
来确保同一时刻仅有一个 goroutine 能够对 counter
变量进行自增操作,从而避免了数据竞争问题。