前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >golang 系列:RWMutex 读写锁分析

golang 系列:RWMutex 读写锁分析

原创
作者头像
lincoln
修改2021-08-10 10:36:24
3750
修改2021-08-10 10:36:24
举报
文章被收录于专栏:后端

摘要

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

读写锁的实现原理

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

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

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

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

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

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

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

RWMutex 源码分析

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

代码语言:txt
复制
// 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() 分析

代码语言:txt
复制
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() 分析

代码语言:txt
复制
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() 分析

代码语言:txt
复制
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() 分析

代码语言:txt
复制
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。


感兴趣的朋友可以搜一搜公众号「 阅新技术 」,关注更多的推送文章。

可以的话,就顺便点个赞、留个言、分享下,感谢各位支持!

阅新技术,阅读更多的新知识。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 摘要
  • 读写锁的实现原理
  • RWMutex 源码分析
  • RWMutex 的 Lock() 分析
  • RWMutex 的 RLock() 分析
  • RWMutex 的 Unlock() 分析
  • RWMutex 的 RUnlock() 分析
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档