Overview

go 里面的 rwlock 是 write preferred 的,可以避免写锁饥饿。

读锁和写锁按照先来后到的规则持有锁,一旦有协程持有了写锁,后面的协程只能在写锁被释放后才能得到读锁。

同样,一旦有 >= 1 个协程写到了读锁,只有等这些读锁全部释放后,后面的协程才能拿到写锁。

下面了解一下 Go 的 RWMutex 是如何实现的吧,下面的代码取自 go1.17.2/src/sync/rwmutex.go,并删减了 race 相关的代码。

PS: rwmutex 的代码挺短的,其实读源码也没那么可怕...

RWMutex 的结构

RWMutex 总体上是通过: 普通锁和条件变量来实现的

type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount int32 // number of pending readers
readerWait int32 // number of departing readers
}
Lock
func (rw *RWMutex) Lock() {
// First, resolve competition with other writers.
rw.w.Lock()
// Announce to readers there is a pending writer.
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
// Wait for active readers.
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false, 0)
}
}
Unlock
const rwmutexMaxReaders = 1 << 30

func (rw *RWMutex) Unlock() {
// Announce to readers there is no active writer.
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
// Unblock blocked readers, if any.
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
// Allow other writers to proceed.
rw.w.Unlock()
}
RLock
func (rw *RWMutex) RLock() {
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// A writer is pending, wait for it.
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
}
RUnlock
func (rw *RWMutex) RUnlock() {
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
// Outlined slow-path to allow the fast-path to be inlined
rw.rUnlockSlow(r)
}
}
func (rw *RWMutex) rUnlockSlow(r int32) {
// A writer is pending.
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
// The last reader unblocks the writer.
runtime_Semrelease(&rw.writerSem, false, 1)
}
}
Q1: 多个协程并发拿读锁,如何保证这些读锁协程都不会被阻塞?
func (rw *RWMutex) RLock() {
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// A writer is pending, wait for it.
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
}

拿读锁时,仅仅会增加 readerCount,因此读锁之间是可以正常并发的

Q2: 多个协程并发拿写锁,如何保证只会有一个协程拿到写锁?
func (rw *RWMutex) Lock() {
// First, resolve competition with other writers.
rw.w.Lock()
// Announce to readers there is a pending writer.
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
// Wait for active readers.
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false, 0)
}
}

拿写锁时,会获取 w.Lock,自然能保证同一时间只会有一把写锁

Q3: 在读锁被拿到的情况下,新协程拿写锁,如果保证写锁现成会被阻塞?
func (rw *RWMutex) Lock() {
// First, resolve competition with other writers.
rw.w.Lock()
// Announce to readers there is a pending writer.
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
// Wait for active readers.
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false, 0)
}
}

假设此时有 5 个协程拿到读锁,则 readerCount = 5,假设 rwmutexMaxReaders = 100。

此时有一个新的协程 w1 想要拿写锁。

在执行

r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders

后, rw.readerCount = -95,r = 5。

在执行

atomic.AddInt32(&rw.readerWait, r)

后,rw.readerWait = 5。

readerWait 记录了在获取写锁的这一瞬间有多少个协程持有读锁。这一瞬间之后,就算有新的协程尝试获取读锁,也只会增加 readerCount ,而不会动到 readerWait。

之后执行 runtime_SemacquireMutex() 睡在了 writerSem 这个信号量上面。

Q4: 在读锁被拿到的情况下,新协程拿写锁被阻塞,当旧有的读锁协程全部释放,如何唤醒等待的写锁协程
func (rw *RWMutex) RUnlock() {
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
// Outlined slow-path to allow the fast-path to be inlined
rw.rUnlockSlow(r)
}
}
func (rw *RWMutex) rUnlockSlow(r int32) {
// A writer is pending.
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
// The last reader unblocks the writer.
runtime_Semrelease(&rw.writerSem, false, 1)
}
}

继续上一步的场景,每当执行 RUnlock 时,readerCount 都会减去1。当 readerCount 为负数时,意味着有协程正在持有或者正在等待持有写锁。

之前的五个读协程中的四个,每次 RUnlock() 之后,readerCount = -95 - 4 = -99,readerWait = 5 - 4 = 1。

当最后一个读协程调用 RUnlock() 之后,readerCount 变成了 -100,readerWait 变成 0,此时会唤醒在 writerSem 上沉睡的协程 w1。

Q5: 在写锁被拿到的情况下,新协程拿读锁,如何让新协程被阻塞?
func (rw *RWMutex) RLock() {
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// A writer is pending, wait for it.
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
}

继续上面的场景,readerCount = -100 + 1 = -99 < 0。

新的读协程 r1 被沉睡在 readerSem 下面。

假设此时再来一个读协程 r2,则 readerCount = -98,依旧沉睡。

Q6: 在写锁被拿到的情况下,新协程拿读锁,写锁协程释放,如何唤醒等待的读锁协程?

继续上面的场景,此时协程 w1 释放写锁

func (rw *RWMutex) Unlock() {
// Announce to readers there is no active writer.
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
// Unblock blocked readers, if any.
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
// Allow other writers to proceed.
rw.w.Unlock()
}

在执行

atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)

后,r = readerCount = -98 + 100 = 2,代表此时有两个读协程 r1 和 r2 在等待

ps: 如果此时有一些新的协程想要拿读锁,他会因为 readerCount = 2 + 1 = 3 > 0 而顺利执行下去,不会被阻塞

之后 for 循环执行两次,将协程 r1 和 协程 r2 都唤醒了。

Q7: 在写锁被拿到的情况下,有两个协程分别去抢读锁和写锁,当写锁被释放时,这两个协程谁会胜利?
func (rw *RWMutex) Unlock() {
// Announce to readers there is no active writer.
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
// Unblock blocked readers, if any.
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
// Allow other writers to proceed.
rw.w.Unlock()
}

由于是先唤醒读锁,再调用 w.Unlock() ,因此肯定是读协程先胜利!

认为写的比较巧妙的两个点
  1. readerCount 与 rwmutexMaxReaders 的纠缠

    通过 readerCount + rwmutexMaxReaders 以及 readerCount - rwmutexMaxReaders 这两个操作可以得知当前是否有协程等待/持有写锁以及当前等待/持有读锁的协程数量

  2. readerCount 与 readerWait 的纠缠

    在 Lock() 时直接将 readerCount 的值赋给 readerWait,在 readerWait = 0 而非 readerCount = 0 是唤醒写协程,可以避免在 Lock() 后来达到的读协程先于写协程被执行。

go RWMutex 的实现的更多相关文章

  1. golang RWMutex读写锁分析

    RWMutex:是基于Mutex实现的读写互斥锁,一个goroutine可以持有多个读锁或者一个写锁,同一时刻只能持有读锁或者写锁 数据结构设计: type RWMutex struct { w Mu ...

  2. go Mutex (互斥锁)和RWMutex(读写锁)

    转载自: https://blog.csdn.net/skh2015java/article/details/60334437 golang中sync包实现了两种锁Mutex (互斥锁)和RWMute ...

  3. Go 初体验 - 并发与锁.1 - sync.Mutex 与 sync.RWMutex

    ==== Mutex为互斥锁,顾名思义,被Mutex锁住的代码同时只允许一个协程访问,其它协程进来就要排队 如何使用?看代码: 输出: 释义: 并发1000个协程同时更改m的元素,这样会有一部分更改成 ...

  4. Go基础系列:互斥锁Mutex和读写锁RWMutex用法详述

    sync.Mutex Go中使用sync.Mutex类型实现mutex(排他锁.互斥锁).在源代码的sync/mutex.go文件中,有如下定义: // A Mutex is a mutual exc ...

  5. go中的读写锁RWMutex

    读写锁是针对于读写操作的互斥锁. 基本遵循两大原则: 1.可以随便读.多个goroutin同时读. 2.写的时候,啥都不能干.不能读,也不能写. 解释: 在32位的操作系统中,针对int64类型值的读 ...

  6. go语言Mutex与RWMutex用法

    Mutex(互斥锁) Lock()加锁,Unlock()解锁 适用于读写不确定,并且只有一个读或者写的场景 例: package main import ( "sync" &quo ...

  7. Golang 读写锁RWMutex 互斥锁Mutex 源码详解

    前言 Golang中有两种类型的锁,Mutex (互斥锁)和RWMutex(读写锁)对于这两种锁的使用这里就不多说了,本文主要侧重于从源码的角度分析这两种锁的具体实现. 引子问题 我一般喜欢带着问题去 ...

  8. golang sync.RWMutex

    sync.RWMutex package main import ( "fmt" "runtime" "sync" ) func click ...

  9. RWMutex:共享/专有的递归互斥锁

    具有共享/独占访问权限,且具有升级/降级功能的互斥锁 介绍 我的目标是创建可以充当读/写锁定机制的对象.任何线程都可以锁定它以进行读取,但是只有一个线程可以锁定它以进行写入.在写入线程释放它之前,所有 ...

  10. golang中sync.RWMutex和sync.Mutex区别

    golang中sync包实现了两种锁Mutex (互斥锁)和RWMutex(读写锁),其中RWMutex是基于Mutex实现的,只读锁的实现使用类似引用计数器的功能. type Mutex     f ...

随机推荐

  1. 资本主义反抗指南精要(v0.1)

    (1)充分预估工作时间,比如一小时的开发任务,你可以加上技术调研,API/数据库设计,单元测试,联调,集成测试等等,拖到一天,同理一天的任务可以拖到一星期. (2)简历上尽一切手段来美化,最好能包装成 ...

  2. 布客·ApacheCN 编程/后端/大数据/人工智能学习资源 2020.11

    公告 我们始终与所有创作者站在一起,为创作自由而战.我们还会提供一切必要的技术支持. 我们全力支持科研开源(DOCX)计划.希望大家了解这个倡议,把这个倡议与自己的兴趣点结合,做点力所能及的事情. 我 ...

  3. [USACO18DEC]Sort It Out P

    初看本题毫无思路,只能从特殊的 \(K = 1\) 出发. 但是直接考虑构造一组字典序最小的方案还是不好构造,可以考虑先手玩一下样例.通过自己手玩的样例可以发现,貌似没有被选出来的数在原排列中都是递增 ...

  4. bom-setInterval

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  5. vue单页面应用打包后相对路径、绝对路径相关问题

    原文链接:  vue单页面应用打包后相对路径.绝对路径相关问题展开       在项目开发过程中,在部署过程中,用到了反向代理,这就要求前端代码中不能使用绝对路径.但是我们知道,一般情况下,通过web ...

  6. DatabaseMetaData

    getColumns public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern ...

  7. DNS域名解析之正向解析

    DNS域名解析之正向解析 1.DNS介绍 2.DNS正向解析实验 1.DNS定义:DNS是"域名系统"的英文缩写.它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地 ...

  8. DHCP原理与LINUX下的配置

    DHCP原理与LINUX下的配置 目录 DHCP原理与LINUX下的配置 一.DHCP工作原理 1.DHCP概述 2.DHCP的优势 3.DHCP的分配方式 (1)自动分配 (2)手动分配 (3)动态 ...

  9. .NET 6全文检索引擎Lucene.NET 4.8简单封装

    前言 因为最近在做一个检索数据的工具.最开始用的Mysql8自带的全文检索功能.但是发现这货数据量超过百万之后,检索速度直线下降. 于是想到Lucene.net.花了一晚上时间做了简单的封装.可以直接 ...

  10. 读源码【读mybatis的源码的思路】

    ✿ 需要掌握的编译器知识 ★ 编译器为eclipse为例子 调试准备工作(步骤:Window -> Show View ->...): □ 打开调试断点Breakpoint: □ 打开变量 ...