文章目录
- 1.互斥锁 sync.Mutex 的实现原理;
- 1.1获取策略有如下两种:
- 1.2sync.Mutex的方案
- 1.2.1具体方案如下:
- 1.2.2转换的条件:
- 1.2.3运行的两种模式:
- 1.2.4两种模式的转换条件
- 1.2.5唤醒标识:
- 1.3源码走读
- 2. sync.RWMutex 的实现原理
- 2.1核心机制
- 2.2读锁源码走读
- 2.3写锁源码走读
1.互斥锁 sync.Mutex 的实现原理;
1.1获取策略有如下两种:
• 阻塞/唤醒:将当前 goroutine 阻塞挂起,直到锁被释放后,以回调的方式将阻塞 goroutine 重新唤醒,进行锁争夺;
• 自旋 + CAS:基于自旋结合 CAS 的方式,重复校验锁的状态并尝试获取锁,始终把主动权握在手中.
上述方案各有优劣,且有其适用的场景:
1.2sync.Mutex的方案
1.2.1具体方案如下:
• 首先保持乐观,goroutine 采用自旋 + CAS 的策略争夺锁;
• 尝试持续受挫达到一定条件后,判定当前过于激烈,则由自旋转为 阻塞/挂起模式.
1.2.2转换的条件:
• 自旋累计达到 4 次仍未取得战果;
• CPU 单核或仅有单个 P 调度器;(此时自旋,其他 goroutine 根本没机会释放锁,自旋纯属空转);
• 当前 P 的执行队列中仍有待执行的 G. (避免因自旋影响到 GMP 调度效率).
1.2.3运行的两种模式:
正常模式/非饥饿模式:这是 sync.Mutex 默认采用的模式. 当有 goroutine 从阻塞队列被唤醒时,会和此时先进入抢锁流程的 goroutine 进行锁资源的争夺,假如抢锁失败,会重新回到阻塞队列头部.(值得一提的是,此时被唤醒的老 goroutine 相比新 goroutine 是处于劣势地位,因为新 goroutine 已经在占用 CPU 时间片,且新 goroutine 可能存在多个,从而形成多对一的人数优势,因此形势对老 goroutine 不利.)
饥饿模式:这是 sync.Mutex 为拯救陷入饥荒的老 goroutine 而启用的特殊机制,饥饿模式下,锁的所有权按照阻塞队列的顺序进行依次传递. 新 goroutine 进行流程时不得抢锁,而是进入队列尾部排队.
1.2.4两种模式的转换条件
• 默认为正常模式;
• 正常模式 -> 饥饿模式:当阻塞队列存在 goroutine 等锁超过 1ms 而不得,则进入饥饿模式;
• 饥饿模式 -> 正常模式:当阻塞队列已清空,或取得锁的 goroutine 等锁时间已低于 1ms 时,则回到正常模式.
小结:
正常模式灵活机动,性能较好;饥饿模式严格死板,但能捍卫公平的底线. 因此,两种模式的切换体现了 sync.Mutex
为适应环境变化,在公平与性能之间做出的调整与权衡. 回头观望,这一项因地制宜、随机应变的能力正是许多优秀工具所共有的特质.
1.2.5唤醒标识:
sync.Mutex 通过一个 mutexWoken 标识位,标志出当前是否已有 goroutine 在自旋抢锁或存在 goroutine 从阻塞队列中被唤醒;倘若 mutexWoken 为 true,且此时有解锁动作发生时,就没必要再额外唤醒阻塞的 goroutine 从而引起竞争内耗。缓解竞争压力和性能损耗。
1.3源码走读
数据结构:
方法:
lock
快速路径通过 CompareAndSwapInt32
尝试以原子的方式获取未被锁定的锁。如果这一步失败,表示锁已被持有或处于某种特殊状态(例如,饥饿模式),则会进入慢速路径 lockSlow
• 首先进行一轮 CAS 操作,假如当前未上锁且锁内不存在阻塞协程,则直接 CAS 抢锁成功返回;
• 第一轮初探失败,则进入 lockSlow 流程,下面细谈.
lockSlow
a.自旋获取锁:
- 如果锁已被持有且未进入饥饿模式,并且当前 goroutine 可以自旋(runtime_canSpin),则进入自旋。
- 尝试设置 mutexWoken 标志,告知 Unlock 不唤醒其他阻塞的 goroutine。
- 执行自旋操作(runtime_doSpin),增加自旋计数并更新 old 状态。
b.更新状态:
- 构建新的状态 new。
- 如果不在饥饿模式,尝试设置 mutexLocked 标志,表示尝试获取锁。
- 如果锁已被持有或处于饥饿模式,增加等待者计数。
- 如果当前进入饥饿模式且锁已被持有,设置 mutexStarving 标志。
- 如果 goroutine 被唤醒,清除 mutexWoken 标志。
c.状态更新成功:
- 如果状态更新成功且锁未被持有或不在饥饿模式,跳出循环,表示成功获取锁。
- 否则,进入阻塞等待(runtime_SemacquireMutex),并更新 starving 状态。
- 如果在饥饿模式下被唤醒,修复不一致的状态,退出饥饿模式。
d.状态更新失败:
- 如果状态更新失败,重新获取锁状态并继续循环。
竞态条件检测: - 如果启用了竞态条件检测,调用 race.Acquire 检查竞态条件。
Unlock
- 通过原子操作解锁;
- 倘若解锁时发现,目前参与竞争的仅有自身一个 goroutine,则直接返回即可;
- 倘若发现锁中还有阻塞协程,则走入 unlockSlow 分支.
unlockSlow - 状态一致性检查:确保解锁操作正确,锁未被意外解锁。
- 非饥饿模式处理:
- 如果没有等待的 goroutine 或锁状态复杂,则不需要进一步操作。
- 如果有等待的 goroutine,尝试更新状态并唤醒一个。
- 饥饿模式处理:
- 直接将锁的所有权传递给下一个等待的 goroutine,并允许其立即运行。
2. sync.RWMutex 的实现原理
2.1核心机制
- 从逻辑上,可以把 RWMutex 理解为一把读锁加一把写锁;
- 写锁具有严格的排他性,当其被占用,其他试图取写锁或者读锁的 goroutine 均阻塞;
- 读锁具有有限的共享性,当其被占用,试图取写锁的 goroutine 会阻塞,试图取读锁的 goroutine 可与当前 goroutine 共享读锁;
- 综上可见,RWMutex 适用于读多写少的场景,最理想化的情况,当所有操作均使用读锁,则可实现去无化;最悲观的情况,倘若所有操作均使用写锁,则 RWMutex 退化为普通的 Mutex.
2.2读锁源码走读
RLock
- 基于原子操作,将 RWMutex 的 readCount 变量加一,表示占用或等待读锁的 goroutine 数加一;
- 倘若 RWMutex.readCount 的新值仍小于 0,说明有 goroutine 未释放写锁,因此将当前 goroutine 添加到读锁的阻塞队列中并阻塞挂起.
RUnlock
- 基于原子操作,将 RWMutex 的 readCount 变量加一,表示占用或等待读锁的 goroutine 数减一;
- 倘若 RWMutex.readCount 的新值小于 0,说明有 goroutine 在等待获取写锁,则走入 RWMutex.rUnlockSlow 的流程中.
rUnlockSlow
- 对 RWMutex.readerCount 进行校验,倘若发现当前协程此前未抢占过读锁,或者介入读锁流程的 goroutine 数量达到上限,则抛出 fatal;
- 基于原子操作,对 RWMutex.readerWait 进行减一操作,倘若其新值为 0,说明当前 goroutine 是最后一个介入读锁流程的协程,因此需要唤醒一个等待写锁的阻塞队列的 goroutine.
2.3写锁源码走读
Lock
- 对 RWMutex 内置的互斥锁进行加锁操作;
- 基于原子操作,对 RWMutex.readerCount 进行减少 -rwmutexMaxReaders 的操作;
- 倘若此时存在未释放读锁的 gouroutine,则基于原子操作在 RWMutex.readerWait 的基础上加上介入读锁流程的 goroutine 数量,并将当前 goroutine 添加到写锁的阻塞队列中挂起.
Unlock
- 基于原子操作,将 RWMutex.readerCount 的值加上 rwmutexMaxReaders;
- 倘若发现 RWMutex.readerCount 的新值大于 rwmutexMaxReaders,则说明要么当前 RWMutex 未上过写锁,要么介入读锁流程的 goroutine 数量已经超限,因此直接抛出 fatal;
- 因此唤醒读锁阻塞队列中的所有 goroutine;(可见,竞争读锁的 goroutine 更具备优势)
- 解开 RWMutex 内置的互斥锁.