golang 系列:RWMutex 读写锁分析

简介: 在上一篇文章 [golang 重要知识:mutex](https://blog.csdn.net/h_l_f/article/details/118462433) 里我们介绍了互斥锁 mutex 的相关原理实现。而且在 Go 里除了互斥锁外,还有读写锁 RWMutex,它主要用来实现读共享,写独占的功能。今天我们也顺便分析下读写锁,加深对 Go 锁的理解。

摘要

在上一篇文章 golang 重要知识:mutex 里我们介绍了互斥锁 mutex 的相关原理实现。而且在 Go 里除了互斥锁外,还有读写锁 RWMutex,它主要用来实现读共享,写独占的功能。今天我们也顺便分析下读写锁,加深对 Go 锁的理解。

读写锁的实现原理

所谓的读写锁,其实就是针对下面的两种场景,对 Goroutine 之间的同步互斥进行控制:

  • 多个 goroutine 一起占有读锁,互不影响,可以继续自己后面的逻辑代码。
  • 写锁正在占有着,则后面的 goroutine 无论是要进行读锁占有,还是写锁占有,都将会被阻塞等待,直到当前的写锁释放。

弄清楚上面的场景需求后,实现就简单多了,关键就在于判断当前是否处于写锁状态即可,毕竟需要有阻塞等待的动作。

按照常规思路,我们一般会采用一个标识位来维护这个状态。然而,Go 官方却连这一步都省了。

利用了一个本来就得维护的读锁数量,在进行写锁占有时,使它变为负数。

后面有新进来的读写操作,只需要判断该值是否正负即可,负数则代表当前正在进行写锁占有,需要阻塞等待。

而在写锁占有结束后,该值又会恢复为正数,又可以进行新的读写操作了。

RWMutex 源码分析

接下来,我们到 src/runtime/rwmutex.go里具体分析下 RWMutex 的代码结构。

// rwmutex 是一个读写互斥的锁
// 将允许多个 goroutine 持有读锁,但写锁只会有一个持有
// rwmutex 使用了 sync.RWMutex 来辅助写锁互斥
type rwmutex struct {
rLock      mutex    // 用于保护设置 readers, readerPass, writer
readers    muintptr // 休眠等待的 goroutine 读锁队列,等到写锁占有结束后将对应被唤起。
readerPass uint32   // 读锁队列需要跳过的 goroutine 数量,当在写锁结束后会唤起读锁队列里的 goroutine,但有的可能已不在队列里了,这部分需跳过。
wLock  mutex    // 用于 writer 之间的互斥锁
writer muintptr // 等待读完成的 writer

readerCount uint32 // 正在执行读操作的 goroutine数量
readerWait  uint32 // 等待读锁释放的数量。当写锁占有后,前面还有部分读锁在继续着,需要等它们释放才能继续进行。
}

RWMutex 的 Lock() 分析

func (rw *rwmutex) Lock() {
    // 用于多个写锁之间的的竞争
    lock(&rw.wLock)
    m := getg().m
    // 将读锁数量 readerCount 置为负数,用于判断当前是否处于写锁占有状态,
    // rw.readerCount < 0 则表示当前正在进行写锁占有.
    r := int32(atomic.Xadd(&rw.readerCount, -rwmutexMaxReaders)) + rwmutexMaxReaders
    // 前面还有读锁在进行着,需要等待释放完才能继续
    lock(&rw.rLock)
    if r != 0 && atomic.Xadd(&rw.readerWait, r) != 0 {
        systemstack(func() {
            rw.writer.set(m)
            unlock(&rw.rLock)
            notesleep(&m.park)
            noteclear(&m.park)
        })
    } else {
        unlock(&rw.rLock)
    }
}

RWMutex 的 RLock() 分析

func (rw *rwmutex) Rlock() {
    acquirem()
    if int32(atomic.Xadd(&rw.readerCount, 1)) < 0 {
        // 读锁数量 readerCount + 1 后小于 0,表示当前正被写锁占有,
        // 等待写锁释放
        systemstack(func() {
            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)
            }
        })
    }
}

RWMutex 的 Unlock() 分析

func (rw *rwmutex) Unlock() {
    // 将原来被写锁置为负数的 readerCount 重新恢复回来.
    r := int32(atomic.Xadd(&rw.readerCount, rwmutexMaxReaders))
    if r >= rwmutexMaxReaders {
        throw("unlock of unlocked rwmutex")
    }
    // 唤起之前等待的读锁.
    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
    }
    // 如果 r > 0, 说明读锁队列里有的 goroutine 已不在队列里了,这部分需跳过
    rw.readerPass += uint32(r)
    unlock(&rw.rLock)
    // 解除写锁
    unlock(&rw.wLock)
}

RWMutex 的 RUnlock() 分析

func (rw *rwmutex) RUnlock() {
    // 如果释放后,readerCount < 0,表示当前写锁正在占有
    if r := int32(atomic.Xadd(&rw.readerCount, -1)); r < 0 {
        if r+1 == 0 || r+1 == -rwmutexMaxReaders {
            throw("runlock of unlocked rwmutex")
        }
        // readerWait == 0,表示前面的读锁都释放完了,
        // 需要唤起写锁
        if atomic.Xadd(&rw.readerWait, -1) == 0 {
            // The last reader unblocks the writer.
            lock(&rw.rLock)
            w := rw.writer.ptr()
            if w != nil {
                notewakeup(&w.park)
            }
            unlock(&rw.rLock)
        }
    }
    releasem(getg().m)
}

总结

RWMutex 通过 readerCount 的正负来判断当前是处于读锁占有还是写锁占有。

在处于写锁占有状态后,会将此时的 readerCount 赋值给 readerWait,表示要等前面 readerWait 个读锁释放完才算完整的占有写锁,才能进行后面的独占操作。

读锁释放的时候, 会对 readerWait 对应减一,直到为 0 值,就可以唤起写锁了。

并且在写锁占有后,即时有新的读操作加进来, 也不会影响到 readerWait 值了,只会影响总的读锁数目:readerCount。

相关文章
|
6月前
|
安全 Go
Golang深入浅出之-互斥锁(sync.Mutex)与读写锁(sync.RWMutex)
【4月更文挑战第23天】Go语言并发编程中,`sync.Mutex`和`sync.RWMutex`是保证线程安全的关键。互斥锁确保单个goroutine访问资源,而读写锁允许多个读者并发访问。常见问题包括忘记解锁、重复解锁以及混淆锁类型。使用`defer`可确保解锁,读写锁不支持直接升级或降级,需释放后再获取。根据读写模式选择合适锁以避免性能下降和竞态条件。理解并正确使用锁是编写并发安全程序的基础。
128 3
|
2月前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
86 4
|
3月前
|
SQL 安全 Java
golang为什么不支持可重入锁?
本文对比分析了Java与Go语言中锁机制的不同。在Java中,无论是`synchronized`关键字还是`ReentrantLock`都支持可重入特性,通过维护一个计数器来跟踪锁的嵌套级别,确保同一线程可以多次获取同一把锁而不会造成死锁。然而,Go语言的`sync.Mutex`并不支持这一特性,其设计理念认为可重入锁往往指向代码设计问题,鼓励开发者重构代码以避免此类需求。文章进一步解释了这种设计理念背后的原因,并提供了替代方案示例。总体而言,Go语言试图从设计层面避免潜在的代码问题,尽管这可能会增加一定的开发复杂性。
golang为什么不支持可重入锁?
|
3月前
|
NoSQL Java 测试技术
Golang内存分析工具gctrace和pprof实战
文章详细介绍了Golang的两个内存分析工具gctrace和pprof的使用方法,通过实例分析展示了如何通过gctrace跟踪GC的不同阶段耗时与内存量对比,以及如何使用pprof进行内存分析和调优。
86 0
Golang内存分析工具gctrace和pprof实战
|
3月前
|
NoSQL Unix 编译器
Golang协程goroutine的调度与状态变迁分析
文章深入分析了Golang中goroutine的调度和状态变迁,包括Grunnable、Gwaiting、Grunning和Gsyscall等状态,以及它们之间的转换条件和原理,帮助理解Go调度器的内部机制。
47 0
|
6月前
|
安全 Go
Golang深入浅出之-Go语言标准库中的文件读写:io/ioutil包
【4月更文挑战第27天】Go语言的`io/ioutil`包提供简单文件读写,适合小文件操作。本文聚焦`ReadFile`和`WriteFile`函数,讨论错误处理、文件权限、大文件处理和编码问题。避免错误的关键在于检查错误、设置合适权限、采用流式读写及处理编码。遵循这些最佳实践能提升代码稳定性。
77 0
|
6月前
|
存储 缓存 算法
Golang高性能内存缓存库BigCache设计与分析
【2月更文挑战第4天】分析Golang高性能内存缓存库BigCache设计
204 0
|
6月前
|
Go
Golang 中的互斥锁是什么?
# go # programming # beginners # architecture
|
10天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
30 2
|
8天前
|
Go 索引
go语言中的循环语句
【11月更文挑战第4天】
19 2