互斥锁 Mutex
底层原理
死锁
互斥锁内部流程
- mu.Lock():
- 当 Goroutine 调用 mu.Lock() 时,首先会检查这个锁的状态:
- 如果锁未被持有,则 Goroutine 成功获得锁,继续执行。
- 如果锁已经被其他 Goroutine 持有,则当前 Goroutine 被挂起,等待锁释放。
- 自旋尝试
- go.18 引入自旋锁 基本思想是,在获取锁时,如果发现锁已经被持有,线程(或 Goroutine)不会立即进入阻塞状态,而是会在一个短时间内反复尝试获取锁,这个过程称为“自旋”。自旋锁特别适用于锁持有时间很短的场景,因为它可以避免线程上下文切换带来的开销。
- 等待队列:
- 当锁被占用时,所有尝试获取锁的 Goroutine 会被放入一个等待队列中(类似于 FIFO 队列)。
- 被挂起的 Goroutine 不会消耗 CPU,Go 运行时会将这些 Goroutine 暂时停止,直到锁被释放。
- mu.Unlock():
- 持有锁的 Goroutine 调用 mu.Unlock() 释放锁时,Go 运行时会从等待队列中唤醒一个等待的 Goroutine,这个 Goroutine 会继续尝试获取锁。
- 如果没有等待的 Goroutine,锁将直接变为可用状态。
饥饿问题
在高并发争抢锁的场景中,新来的goroutine 也参与竞争,有可能每次都是被新的goroutine 获得锁的机会,在极端场景下,等待中的goroutine可能一直获取不到锁。
go1.9中 mutex 增加来饥饿模式(mutexStarving),让获取锁更加公平,不公平的等待时间限制在 1ms
饥饿模式的工作原理:
- 当一个goroutine等待锁超过1毫秒时,mutex会切换到饥饿模式。
- 在饥饿模式下,mutex的所有权会直接从解锁的goroutine转移到等待时间最长的goroutine。
- 新来的goroutine不会尝试获取锁,即使锁看起来是空闲的,它们会排到等待队列的尾部。
饥饿模式的退出:
- 当一个等待的goroutine获取了锁,并且满足以下条件之一时,mutex会退出饥饟模式:
- a) 它是队列中的最后一个等待者,或者
- b) 它等待的时间少于1毫秒
饥饿模式的优点:
- 确保了锁分配的公平性,防止了长时间的饥饿现象。
- 提高了在高并发情况下的性能表现。