图解golang里面的读写锁实现与核心原理分析了解编程语言背后设计
基础筑基
读写锁的特点
读写锁区别与互斥锁的主要区别就是读锁之间是共享的,多个goroutine可以同时加读锁,但是写锁与写锁、写锁与读锁之间则是互斥的
写锁饥饿问题
因为读锁是共享的,所以如果当前已经有读锁,那后续goroutine继续加读锁正常情况下是可以加锁成功,但是如果一直有读锁进行加锁,那尝试加写锁的goroutine则可能会长期获取不到锁,这就是因为读锁而导致的写锁饥饿问题
基于高低位与等待队列的实现
在说golang之前介绍一种JAVA里面的实现,在JAVA中ReentrantReadWriteLock实现采用一个state的高低位来进行读写锁的计数,其中高16位存储读的计数,低16位存储写的计数,并配合一个AQS来实现排队等待机制,同时AQS中的每个waiter都会有一个status,用来标识自己的状态
golang的读写锁的实现
成员变量
结构体
type RWMutex struct { w Mutex // held if there are pending writers writerSem uint32 // 用于writer等待读完成排队的信号量 readerSem uint32 // 用于reader等待写完成排队的信号量 readerCount int32 // 读锁的计数器 readerWait int32 // 等待读锁释放的数量 }
写锁计数
读写锁中允许加读锁的最大数量是4294967296,在go里面对写锁的计数采用了负值进行,通过递减最大允许加读锁的数量从而进行写锁对读锁的抢占
const rwmutexMaxReaders = 1 << 30
读锁实现
读锁加锁逻辑
func (rw *RWMutex) RLock() { if race.Enabled { _ = rw.w.state race.Disable() } // 累加reader计数器,如果小于0则表明有writer正在等待 if atomic.AddInt32(&rw.readerCount, 1) < 0 { // 当前有writer正在等待读锁,读锁就加入排队 runtime_SemacquireMutex(&rw.readerSem, false) } if race.Enabled { race.Enable() race.Acquire(unsafe.Pointer(&rw.readerSem)) } }
读锁释放逻辑
func (rw *RWMutex) RUnlock() { if race.Enabled { _ = rw.w.state race.ReleaseMerge(unsafe.Pointer(&rw.writerSem)) race.Disable() } // 如果小于0,则表明当前有writer正在等待 if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 { if r+1 == 0 || r+1 == -rwmutexMaxReaders { race.Enable() throw("sync: RUnlock of unlocked RWMutex") } // 将等待reader的计数减1,证明当前是已经有一个读的,如果值==0,则进行唤醒等待的 if atomic.AddInt32(&rw.readerWait, -1) == 0 { // The last reader unblocks the writer. runtime_Semrelease(&rw.writerSem, false) } } if race.Enabled { race.Enable() } }
写锁实现
加写锁实现
func (rw *RWMutex) Lock() { if race.Enabled { _ = rw.w.state race.Disable() } // 首先获取mutex锁,同时多个goroutine只有一个可以进入到下面的逻辑 rw.w.Lock() // 对readerCounter进行进行抢占,通过递减rwmutexMaxReaders允许最大读的数量 // 来实现写锁对读锁的抢占 r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders // 记录需要等待多少个reader完成,如果发现不为0,则表明当前有reader正在读取,当前goroutine // 需要进行排队等待 if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 { runtime_SemacquireMutex(&rw.writerSem, false) } if race.Enabled { race.Enable() race.Acquire(unsafe.Pointer(&rw.readerSem)) race.Acquire(unsafe.Pointer(&rw.writerSem)) } }
释放写锁
func (rw *RWMutex) Unlock() { if race.Enabled { _ = rw.w.state race.Release(unsafe.Pointer(&rw.readerSem)) race.Disable() } // 将reader计数器复位,上面减去了一个rwmutexMaxReaders现在再重新加回去即可复位 r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders) if r >= rwmutexMaxReaders { race.Enable() throw("sync: Unlock of unlocked RWMutex") } // 唤醒所有的读锁 for i := 0; i < int(r); i++ { runtime_Semrelease(&rw.readerSem, false) } // 释放mutex rw.w.Unlock() if race.Enabled { race.Enable() } }
关键核心机制
写锁对读锁的抢占
加写锁的抢占
// 在加写锁的时候通过将readerCount递减最大允许加读锁的数量,来实现对加读锁的抢占 r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
加读锁的抢占检测
// 如果没有写锁的情况下读锁的readerCount进行Add后一定是一个>0的数字,这里通过检测值为负数 //就实现了读锁对写锁抢占的检测 if atomic.AddInt32(&rw.readerCount, 1) < 0 { // A writer is pending, wait for it. runtime_SemacquireMutex(&rw.readerSem, false) }
写锁抢占读锁后后续的读锁就会加锁失败,但是如果想加写锁成功还要继续对已经加读锁成功的进行等待
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 { // 写锁发现需要等待的读锁释放的数量不为0,就自己自己去休眠了 runtime_SemacquireMutex(&rw.writerSem, false) }
写锁既然休眠了,则必定要有一种唤醒机制其实就是每次释放锁的时候,当检查到有加写锁的情况下,就递减readerWait,并由最后一个释放reader lock的goroutine来实现唤醒写锁
if atomic.AddInt32(&rw.readerWait, -1) == 0 { // The last reader unblocks the writer. runtime_Semrelease(&rw.writerSem, false) }
写锁的公平性
在加写锁的时候必须先进行mutex的加锁,而mutex本身在普通模式下是非公平的,只有在饥饿模式下才是公平的
rw.w.Lock()
写锁与读锁的公平性
在加读锁和写锁的工程中都使用atomic.AddInt32来进行递增,而该指令在底层是会通过LOCK来进行CPU总线加锁的,因此多个CPU同时执行readerCount其实只会有一个成功,从这上面看其实是写锁与读锁之间是相对公平的,谁先达到谁先被CPU调度执行,进行LOCK锁cache line成功,谁就加成功锁
可见性与原子性问题
在并发场景中特别是JAVA中通常会提到并发里面的两个问题:可见性与内存屏障、原子性, 其中可见性通常是指在cpu多级缓存下如何保证缓存的一致性,即在一个CPU上修改了了某个数据在其他的CPU上不会继续读取旧的数据,内存屏障通常是为了CPU为了提高流水线性能,而对指令进行重排序而来,而原子性则是指的执行某个操作的过程的不可分割
底层实现的CPU指令
go里面并没有volatile这种关键字,那如何能保证上面的AddInt32这个操作可以满足上面的两个问题呢, 其实关键就在于底层的2条指令,通过LOCK指令配合CPU的MESI协议,实现可见性和内存屏障,同时通过XADDL则用来保证原子性,从而解决上面提到的可见性与原子性问题
// atomic/asm_amd64.s TEXT runtime∕internal∕atomic·Xadd(SB) LOCK XADDL AX, 0(BX)
更多文章可以访问www.sreguide.com 本篇文章由一文多发平台ArtiPub自动发布
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
企业安全应从防御攻击转向遏制攻击吗?
网络攻击的风险每年倍增,CIO 和 CISO 是否应重新思考他们组织的 IT 安全方法?重点放在攻击的预防(事前)?还是遏制(事中和事后)? 说到对抗网络犯罪,重点往往一边倒向预防。 但仅去年一年,针对企业的攻击规模就增加 122% ,充分暴露出公司企业在对抗网络犯罪上的艰难处境。一旦有攻击发生,重点很快就会转向遏制攻击,并确保损害尽可能小。但企业该如何有效做到这一点呢? 发展出全面的方法 网络犯罪威胁的规模和复杂度均在增长。没有哪种特定方法能够根除所有风险。 网络态势的不断发展和人们对技术的日渐依赖,意味着数据防护是所有公司、行业和地区的关注重点。 仅偏向遏制或仅偏向防御,都不会是成功的网络风险管理方案。 企业所具备的及时检测并有效响应攻击的能力,对缓解潜在的金融、声誉或合规灾难起到了至关重要的作用。 无论如何,不能仅仅因为市场偏向这个方向,就错误地从防御攻击完全转向管理攻击。 企业应采取全面的网络威胁方案,来提供可以抵御潜在的商业诈骗或网络犯罪所需的全部对策。 这些安全对策需要根据企业风险的优先级排序,并考虑纳入公司的业务风险范畴。 其中一些对策可以减轻网络威胁概率,比如说加密和强...
- 下一篇
图解Go里面的互斥锁mutex了解编程语言核心实现源码
1. 锁的基础概念 1.1 CAS与轮询 1.1.1 cas实现锁 在锁的实现中现在越来越多的采用CAS来进行,通过利用处理器的CAS指令来实现对给定变量的值交换来进行锁的获取 1.1.2 轮询锁 在多线程并发的情况下很有可能会有线程CAS失败,通常就会配合for循环采用轮询的方式去尝试重新获取锁 1.2 锁的公平性 锁从公平性上通常会分为公平锁和非公平锁,主要取决于在锁获取的过程中,先进行锁获取的线程是否比后续的线程更先获得锁,如果是则就是公平锁:多个线程按照获取锁的顺序依次获得锁,否则就是非公平性 1.3 饥饿与排队 1.3.1 锁饥饿 锁饥饿是指因为大量线程都同时进行获取锁,某些线程可能在锁的CAS过程中一直失败,从而长时间获取不到锁 1.3.2 排队机制 上面提到了CAS和轮询锁进行锁获取的方式,可以发现如果已经有线程获取了锁,但是在当前线程在多次轮询获取锁失败的时候,就没有必要再继续进行反复尝试浪费系统资源,通常就会采用一种排队机制,来进行排队等待 1.4 位计数 在大多数编程语言中针对实现基于CAS的锁的时候,通常都会采用一个32位的整数来进行锁状态的存储 2. mutex...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- MySQL8.0.19开启GTID主从同步CentOS8
- 设置Eclipse缩进为4个空格,增强代码规范
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Hadoop3单机部署,实现最简伪集群
- CentOS8安装Docker,最新的服务器搭配容器使用
- Docker快速安装Oracle11G,搭建oracle11g学习环境