Lock

类型

互斥锁 (Mutex)

1var mu sync.Mutex
2mu.Lock()
3// 临界区代码
4mu.Unlock()
  • 同一时刻只有一个goroutine能持有锁
  • 零值可用(未锁定的mutex)
  • 不可重入(同一goroutine重复Lock会死锁)

读写锁 (RWMutex)

1var rwmu sync.RWMutex
2rwmu.RLock()   // 读锁
3rwmu.RUnlock() // 读操作
4
5rwmu.Lock()   // 写锁
6rwmu.Unlock() // 写操作
  • 读-读不互斥
  • 读-写互斥
  • 写-写互斥
  • 适合读多写少的场景

其他同步原语

  • sync.Once:确保操作只执行一次
  • sync.WaitGroup:等待一组goroutine完成
  • sync.Cond:条件变量
  • sync.Map:并发安全的map
  • atomic包:原子操作

特性

公平

源码

 1const (
 2	mutexLocked = 1 << iota // mutex is locked
 3	mutexWoken
 4	mutexStarving
 5	mutexWaiterShift = iota
 6
 7	// Mutex fairness.
 8	//
 9	// Mutex can be in 2 modes of operations: normal and starvation.
10	// In normal mode waiters are queued in FIFO order, but a woken up waiter
11	// does not own the mutex and competes with new arriving goroutines over
12	// the ownership. New arriving goroutines have an advantage -- they are
13	// already running on CPU and there can be lots of them, so a woken up
14	// waiter has good chances of losing. In such case it is queued at front
15	// of the wait queue. If a waiter fails to acquire the mutex for more than 1ms,
16	// it switches mutex to the starvation mode.
17	//
18	// In starvation mode ownership of the mutex is directly handed off from
19	// the unlocking goroutine to the waiter at the front of the queue.
20	// New arriving goroutines don't try to acquire the mutex even if it appears
21	// to be unlocked, and don't try to spin. Instead they queue themselves at
22	// the tail of the wait queue.
23	//
24	// If a waiter receives ownership of the mutex and sees that either
25	// (1) it is the last waiter in the queue, or (2) it waited for less than 1 ms,
26	// it switches mutex back to normal operation mode.
27	//
28	// Normal mode has considerably better performance as a goroutine can acquire
29	// a mutex several times in a row even if there are blocked waiters.
30	// Starvation mode is important to prevent pathological cases of tail latency.
31	starvationThresholdNs = 1e6
32)

这些常量用于表示互斥锁的不同状态,通过位掩码方式存储在锁的内部状态中:

  1. mutexLocked (1 « 0 = 1) - 表示锁已被占用
  2. mutexWoken (1 « 1 = 2) - 表示有 goroutine 被唤醒
  3. mutexStarving (1 « 2 = 4) - 表示锁处于饥饿模式
  4. mutexWaiterShift (值为3) - 表示等待 goroutine 数量的位移量

工作模式

正常模式 (Normal Mode)
  • 等待的 goroutine 按照 FIFO 顺序排队
  • 被唤醒的 waiter 需要与新到达的 goroutine 竞争锁
  • 新到达的 goroutine 有优势:因为它们已经在 CPU 上运行,数量可能很多
  • 如果被唤醒的 waiter 竞争失败,会被重新放到队列前面
  • 如果 waiter 等待超过 1ms 还获取不到锁,则切换到饥饿模式
饥饿模式 (Starvation Mode)
  • 锁的所有权直接从解锁的 goroutine 移交给队列前面的 waiter
  • 新到达的 goroutine 不会尝试获取锁,即使锁看起来是未锁定的
  • 新到达的 goroutine 也不会自旋,而是直接排到队列尾部
  • 当以下条件满足时,切换回正常模式:
    • 当前 waiter 是队列中最后一个
    • 当前 waiter 等待时间少于 1ms(starvationThresholdNs 定义了 阈值时间)

设计考量

  1. 性能优先:正常模式下,即使有阻塞的 waiter,goroutine 也可以连续多次获取锁,性能更好
  2. 防止饥饿:饥饿模式防止了尾部延迟的极端情况,确保没有 goroutine 会无限期等待
  3. 自适应:根据实际情况在两种模式间自动切换,平衡性能和公平性

这种设计使得 Go 的互斥锁在大多数高性能场景下表现良好,同时在极端情况下也能保证公平性。

TryLock

尝试获取锁,获取不到也不堵塞

 1// TryLock tries to lock m and reports whether it succeeded.
 2//
 3// Note that while correct uses of TryLock do exist, they are rare,
 4// and use of TryLock is often a sign of a deeper problem
 5// in a particular use of mutexes.
 6func (m *Mutex) TryLock() bool {
 7	old := m.state
 8	if old&(mutexLocked|mutexStarving) != 0 {
 9		return false
10	}
11
12	// There may be a goroutine waiting for the mutex, but we are
13	// running now and can try to grab the mutex before that
14	// goroutine wakes up.
15	if !atomic.CompareAndSwapInt32(&m.state, old, old|mutexLocked) {
16		return false
17	}
18
19	if race.Enabled {
20		race.Acquire(unsafe.Pointer(m))
21	}
22	return true
23}

Go1.18 引入的新特性,但是 rsc 并不推荐使用它,他认为 TryLock 会鼓励设计者对锁进行不精确的思考,这可能最终会成为 race(竞态) 的根源。

参考

  1. What does “hot path” mean in the context of sync.Once?

  2. sync.mutex 源代码分析

  3. Go1.18 新特性:TryLock