Go 并发控制之 Mutex 与 RWMutex

发布于:2025-07-27 ⋅ 阅读:(14) ⋅ 点赞:(0)

Go 并发控制之 MutexRWMutex

一、Mutex 包的功能和用法

(一)功能概述

sync.Mutex 是 Go 提供的对共享资源进行互斥访问的原语。它的核心作用是确保同一时刻只有一个 goroutine 能够持有锁,从而避免多个 goroutine 同时访问共享资源导致的数据混乱。

(二)基本用法

  • Mutex 仅对外提供两个方法:Lock()Unlock()Lock() 用于加锁,当一个 goroutine 调用 Lock() 方法时,如果锁未被其他 goroutine 持有,则该 goroutine 成功获取锁;如果锁已经被其他 goroutine 持有,则当前 goroutine 会阻塞,直到获取到锁。Unlock() 则用于解锁,释放锁资源,以便其他等待的 goroutine 可以获取锁。

(三)四种状态

  • Locked :表示 Mutex 是否已被锁定。值为 0 表示未锁定,值为 1 表示已被锁定。这是判断锁是否可用的关键状态。
  • Woken :用于指示是否有协程已被唤醒且正在尝试加锁。值为 0 表示没有协程唤醒,值为 1 表示有协程被唤醒。它有助于协调多个 goroutine 对锁的竞争。
  • Starving :反映 Mutex 是否处于饥饿状态。当有协程阻塞超过 1ms 时,Mutex 会进入饥饿模式,此时该字段值为 1。饥饿模式下,锁的获取策略会有所调整,以避免某些 goroutine 长时间无法获取锁。
  • Waiter :记录阻塞等待锁的协程个数。在协程解锁时,根据这个值来判断是否需要释放信号量,从而唤醒等待的协程。

(四)自旋机制

当一个 goroutine 尝试获取一个已经被其他 goroutine 持有的锁时,并不会立即进入阻塞状态。而是会进入一个自旋过程,持续探测锁的 Locked 位是否变为 0。自旋的好处在于,如果锁很快就会被释放,自旋的 goroutine 就有机会在短时间内获取锁,避免了协程的频繁切换,提高了效率。

(五)正常模式和饥饿模式

  • 正常模式 :在正常模式下,goroutine 加锁失败时,会先判断是否满足自旋条件。如果满足,就会启动自旋过程,尝试抢锁。这种模式适用于锁竞争不激烈的情况,能够充分利用自旋的优势,减少协程切换开销。
  • 饥饿模式 :如果一个 goroutine 被阻塞后,在上次阻塞到本次阻塞的时间超过 1ms,就会将 Mutex 标记为饥饿模式。饥饿模式下,不再启动自旋过程,而是直接进入阻塞队列等待锁的释放。这种模式主要用于应对锁竞争非常激烈的情况,确保每个等待的 goroutine 都能在一定时间内获取到锁,避免出现部分 goroutine 长时间无法获取锁的 “饥饿” 现象。

二、RWMutex 是什么

(一)概念

sync.RWMutex 是 Go 标准库中的读写互斥锁。它允许多个 goroutine 同时读取共享资源,但在写操作时必须独占锁。这使得 RWMutex 在读多写少的高并发场景中表现出色,能够有效提高程序的并发性能。

(二)功能实现

  • RWMutex 内部通过一个读计数器和一个写计数器来实现其功能。读计数器用于记录当前正在进行的读操作数量,当有多个 goroutine 同时读取资源时,读计数器会相应增加。只有当读计数器为 0 时,才允许写操作进行。写计数器则用于记录当前是否有写操作正在进行,一旦有写操作获取锁,写计数器会增加,此时其他所有读写操作都会被阻塞。
  • 它提供了两种锁机制:读锁(RLock()RUnlock())以及写锁(Lock()Unlock())。读锁允许多个 goroutine 同时持有,而写锁是独占的,同一时刻只能有一个 goroutine 持有写锁。

(三)使用注意事项

  • 成对调用锁与解锁 :无论是读锁还是写锁,都必须成对调用相应的锁和解锁方法。否则,可能会导致死锁或者程序出现 panic。例如,调用了 RLock() 后没有对应地调用 RUnlock(),或者调用了 Lock() 后没有调用 Unlock(),都会导致锁无法正常释放,进而引发死锁。
  • 不可锁升级 :在持有 RLock() 的情况下,不能尝试获取 Lock()。因为读锁允许多个 goroutine 同时持有,而写锁需要独占,如果允许锁升级,会导致写操作与其他读操作冲突,从而引发死锁。
  • 写锁不能重入RWMutex 不支持写锁的重入。这意味着,如果一个 goroutine 已经持有了写锁,再次调用 Lock() 方法时,会导致死锁。因为写锁的设计初衷是为了保证写操作的独占性,重入写锁会破坏这一特性。
  • 不记录 goroutineRWMutex 不会记录是哪个 goroutine 加了锁。因此,在解锁时,必须确保是在正确的 goroutine 中进行解锁操作,否则会导致解锁失败或者出现其他不可预见的问题,比如跨 goroutine 解锁可能会引发 panic。
  • 小心遗漏解锁 :忘记调用 Unlock()RUnlock() 方法来释放锁,会导致锁一直被持有,其他需要该锁的 goroutine 无法获取锁,从而引发死锁或者资源泄漏。例如,在函数执行过程中,如果没有在所有可能的分支路径上都正确地调用解锁方法,就可能出现遗漏解锁的情况。
  • 解锁后的唤醒机制 :对写锁解锁时,会唤醒所有因试图锁定读锁而被阻塞的 goroutine,因为写锁释放后,读操作可以重新开始进行。而对读锁解锁时,只有在没有其他读锁锁定的前提下,才会唤醒因试图锁定写锁而被阻塞的 goroutine,这是为了确保写操作能够在没有其他读操作干扰的情况下进行。

网站公告

今日签到

点亮在社区的每一天
去签到