Go语言读写锁(RWMutex)底层原理详解

Go语言读写锁(RWMutex)底层原理详解

概述

Go语言的sync.RWMutex是一种读写锁,允许多个读操作同时进行,但写操作是互斥的。这种锁机制在读多写少的场景下能显著提高并发性能。底层通过互斥锁和原子计数器实现复杂的并发控制。

核心数据结构

type rwmutex struct {
    rLock      mutex    // 保护读操作相关的数据(readers, writer)
    readers    muintptr // 等待读操作的队列
    wLock      mutex    // 控制写操作的互斥锁
    writer     muintptr // 等待完成读操作的写操作
    readerCount atomic.Int32 // 当前有多少个正在进行的读操作
    readerWait  atomic.Int32 // 写操作需要等待的读操作的数量
    readerPass  int32     // 读通过计数
}

字段说明

  • rLock: 保护读操作相关的数据(readers, writer队列)
  • readers: 等待读操作的队列(muintptr链表结构)
  • wLock: 控制写操作的互斥锁,确保写操作互斥
  • writer: 等待完成读操作的写操作指针
  • readerCount: 当前有多少个正在进行的读操作,关键设计:负数表示有写操作竞争
  • readerWait: 写操作需要等待的读操作的数量
  • readerPass: 写操作完成后允许通过的读操作数

状态转换机制

1. rlock() 获取读锁流程

rlock()操作流程:

func (rw *rwmutex) rlock() {
    // 增加当前读操作的计数,如果 +1 还小于0,说明目前有写锁竞争
    if rw.readerCount.Add(1) < 0 {
        // 如果有写操作等待,则当前读操作需要挂起,排队等待写操作完成
        systemstack(func() {
            // 获取rLock锁
            lock(&rw.rLock)
            if rw.readerPass > 0 {
                // 如果写操作已经完成,跳过当前读操作
                rw.readerPass -= 1
                unlock(&rw.rLock)
            } else {
                // 将当前读操作加入等待队列,等待写操作释放锁
                m := getg().m
                m.schedlink = rw.readers
                rw.readers.set(m)
                unlock(&rw.rLock)
                // 当前读操作挂起,等待写操作唤醒
                notesleep(&m.park)
                noteclear(&m.park)
            }
        })
    }
}

关键点:

  • 原子操作递增readerCount
  • 如果结果为负数,说明有写者在等待,当前读者需要阻塞
  • 这种设计实现了写者优先的策略

2. runlock() 解锁读锁流程

runlock()操作流程:

func (rw *rwmutex) runlock() {
    // 如果r小于0说明此时有writer竞争
    if r := rw.readerCount.Add(-1); r < 0 {
        if r+1 == 0 || r+1 == -rwmutexMaxReaders {
            throw("runlock of unlocked rwmutex")
        }
        // 将等待reader的计数减1,如果值==0,说明读等待都处理完了
        // 此时需要唤醒写等待
        if rw.readerWait.Add(-1) == 0 {
            lock(&rw.rLock)
            w := rw.writer.ptr()
            if w != nil {
                notewakeup(&w.park)
            }
            unlock(&rw.rLock)
        }
    }
}

关键点:

  • 原子操作递减readerCount
  • 如果递减后为负数,说明有写者在等待
  • readerWait减到0时,唤醒等待的写者

3. lock() 获取写锁流程

lock()操作流程:

const rwmutexMaxReaders = 1 << 30

func (rw *rwmutex) lock() {
    // 获取写互斥锁,确保只有一个写操作可以执行
    lock(&rw.wLock)
    m := getg().m
    // 将readerCount - 最大读锁数,肯定得到一个负数
    // 不会丢失当前正在进行的读操作数量,又可以将值设置为负数
    r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
    // 获取读锁
    lock(&rw.rLock)
    // 如果有读操作正在进行,写操作需要等待它们完成
    if r != 0 && rw.readerWait.Add(r) != 0 {
        // 等待读操作完成后才能执行写操作
        systemstack(func() {
            // 设置当前写操作为等待状态
            rw.writer.set(m)
            unlock(&rw.rLock)
            // 当前写操作挂起,等待读操作唤醒
            notesleep(&m.park)
            noteclear(&m.park)
        })
    } else {
        unlock(&rw.rLock)
    }
}

关键点:

  • 先获取基础互斥锁w
  • readerCount减去一个很大的数(rwmutexMaxReaders),使其变为负数
  • 等待现有读者完成(通过readerWait计数)

4. unlock() 解锁写锁流程

unlock()操作流程:

func (rw *rwmutex) unlock() {
    // 将readerCount复原,表示当前写操作已经完成
    r := rw.readerCount.Add(rwmutexMaxReaders)
    if r >= rwmutexMaxReaders {
        // 如果没有锁被持有,抛出异常
        throw("unlock of unlocked rwmutex")
    }
    // 获取rLock锁,操作读者队列
    lock(&rw.rLock)
    // 遍历并唤醒所有在读者队列中的等待操作
    for rw.readers.ptr() != nil {
        reader := rw.readers.ptr()
        rw.readers = reader.schedlink
        reader.schedlink.set(nil)
        notewakeup(&reader.park)
        r -= 1
    }
    unlock(&rw.rLock)
    // 释放写锁,允许其他写操作进行
    unlock(&rw.wLock)
}

关键点:

  • readerCount恢复为正数
  • 唤醒所有等待的读者
  • 最后释放基础互斥锁

优先级策略

写者优先机制

Go的RWMutex实现了写者优先的策略,这是通过以下机制实现的:

  1. 负数标记:当写者到来时,将readerCount设置为负数
  2. 新读者阻塞:新来的读者发现readerCount为负数时会阻塞
  3. 写者等待:写者会等待现有读者完成,但不会让新读者"插队"

避免饥饿

这种设计避免了写者饥饿问题:

  • 如果不断有新读者到来,写者可能会永远等待
  • 通过负数标记,新读者会被阻塞,确保写者最终能获得锁

并发控制流程

读操作并发

graph TD A[Reader1请求读锁] --> B[readerCount++] C[Reader2请求读锁] --> D[readerCount++] B --> E[Reader1获得读锁] D --> F[Reader2获得读锁] E --> G[Reader1释放读锁] F --> H[Reader2释放读锁] G --> I[readerCount--] H --> J[readerCount--]

读写互斥

graph TD A[Reader持有读锁] --> B[Writer请求写锁] B --> C[readerCount设为负数] C --> D[Writer等待] A --> E[Reader释放读锁] E --> F[readerCount--] F --> G[检查readerWait] G --> H[唤醒Writer] H --> I[Writer获得写锁]

性能特点

优点

  1. 高并发读:多个读操作可以同时进行
  2. 写者优先:避免写者饥饿
  3. 公平性:在读写竞争中保持相对公平

缺点

  1. 写操作开销:写操作需要等待所有读者完成
  2. 内存开销:需要维护多个计数器和信号量
  3. 复杂度:实现逻辑相对复杂

使用场景

适合场景

  • 读多写少:如配置文件读取、缓存查询
  • 读操作耗时短:快速读取,避免长时间持有锁
  • 写操作不频繁:偶尔的更新操作

不适合场景

  • 写操作频繁:会导致读者频繁阻塞
  • 读操作耗时:长时间持有读锁会影响写操作
  • 需要严格公平:某些场景可能需要更复杂的公平策略

总结

Go语言的RWMutex通过巧妙的设计实现了高效的读写锁机制:

  1. 原子操作:使用原子操作保证计数器的准确性
  2. 信号量机制:通过信号量实现等待/唤醒机制
  3. 负数标记:用负数表示写者等待状态
  4. 写者优先:避免写者饥饿问题

这种设计在读多写少的场景下能显著提高并发性能,是Go并发编程中的重要工具。

注意:在实际使用中,要避免在持有锁时进行耗时操作,以免影响其他goroutine的执行。同时,要确保锁的正确释放,避免死锁。

posted @ 2025-09-12 07:49  元贞  阅读(10)  评论(0)    收藏  举报