面试官:哥们,Go语言的读写锁了解多少?

网站建设3年前发布
66 0 0

互斥锁我们都知道会锁定代码临界区,当有一个goroutine获取了互斥锁后,任何goroutine都不可以获取互斥锁,只能等待这个goroutine将互斥锁释放,无论读写操作都会加上一把大锁,在读多写少场景效率会很低,所以大佬们就设计出了读写锁,读写锁顾名思义是一把锁分为两部分:读锁和写锁,读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的,总结来说:读读不互斥,读写互斥,写写互斥。,有些朋友可能会有疑惑,为什么要有读锁,读操作又不会修改数据,多线程同时读取相同的资源就是安全的,为什么还要加一个读锁呢?,举个例子说明,在Golang中变量的赋值不是并发安全的,比如对一个int型变量执行count++操作,在并发下执行就会出现预期之外的结果,因为count++操作分为三部分:读取count的值、将count的值加1,然后再将结果赋值给count,这不是一个原子性操作,未加锁时在多个线程同时对该变量执行count++操作会造成数据不一致,通过加上写锁可以解决这个问题,但是在读取的时候我们不加读锁会怎么样呢?写个例子来看一下,只加写锁,不加读锁:,运行结果:,每次运行结果都是不固定的,因为我们没有加读锁,如果允许同时读和写,读取到的数据有可能就是中间状态,所以我们可以总结出来读锁是很有必要的,读锁可以防止读到写中间的值。,多个读操作同时进行时也是线程安全的,一个线程获取读锁后,另外一个线程同样可以获取读锁,因为读锁是共享的,如果一直都有线程加读锁,后面再有线程加写锁就会一直获取不到锁造成阻塞,这时就需要一些策略来保证锁的公平性,避免出现锁饥饿,那么Go语言中读写锁采用的是什么插队策略来避免饥饿问题呢?,这里我们用一个例子来说明一下Go语言的插队策略:,假设现在有5个goroutine分别是G1、G2、G3、G4、G5,现在G1、G2获取读锁成功,还没释放读锁,G3要执行写操作,获取写锁失败就会阻塞等待,当前阻塞写锁的读锁goroutine数量为2:,20230306120734597e776617c12186956600e59f6ae5c18d2fd5100,后续G4进来想要获取读锁,这时她就会判断如果当前有写锁的goroutine正在阻塞等待,为了避免写锁饥饿,那这个G4也会进入阻塞等待,后续G5进来想要获取写锁,因为G3在占用互斥锁,所以G5会进入自旋/休眠 阻塞等待;,2023030610550782a236685dfbc77986d686b4f245555a85fbae111,现在G1、G2释放了读锁,当释放读锁是判断如果阻塞写锁goroutine的读锁goroutine数量为0了并且有写锁等待就会唤醒正在阻塞等待的写锁G3,G3得到了唤醒:,20230306105507f316d330762914dafbc721277576a08c0d7135988,G3处理完写操作后会释放写锁,这一步会同时唤醒等待的读锁/写锁的goroutine,至于G4、G5谁能先获取锁就看谁比较快了,就像抢媳妇一样,先下手的先得呀。,接下来我们就深入源码分析一下,先看一下RWMutex结构都有啥:,读锁的对应方法如下:,精简了竞态检测的方法,读锁方法就只有两行代码了,逻辑如下:,使用原子操作更新readerCount,将readercount值加1,只要原子操作后值不为负数就表示加读锁成功,如果值为负数表示已经有写锁获取互斥锁成功,写锁goroutine正在等待或运行,所以为了避免饥饿后面进来的读锁要进行阻塞等待,调用runtime_SemacquireMutex阻塞等待。,Go语言在1.18中引入了非阻塞加读锁的方法:,因为读锁是共享的,在没有写锁阻塞等待时多个线程可以同时获取,所以原子性操作可能会失败,这里采用for循环来增加尝试次数,很是巧妙。,释放读锁代码主要分为两部分,第一部分:,我们都知道readerCount的值代表当前正在执行的读操作goroutine数量,执行递减操作后的值大于等于0表示当前没有异常场景或写锁阻塞等待,所以直接退出即可,否则需要处理这两个逻辑:,rUnlockSlow逻辑如下:,解读一下这段代码:,写锁对应的方法如下:,代码量不是很大,但是理解起来还有一点复杂,我尝试用文字来解析一下,主要分为两部分:,Go语言在1.18中引入了非阻塞加锁的方法:,释放写锁的逻辑比较简单,释放写锁会将会面的读操作和写操作的goroutine都唤醒,然后他们在进行竞争。,因为我们上文已经分享了互斥锁的实现方式,再来看读写锁就轻松许多了,文末我们再来总结一下读写锁:,锁空闲时,读锁可以立马被获取,如果当前有写锁正在阻塞,那么想要获取读锁的goroutine就会被休眠,当前没有异常场景或写锁阻塞等待出现的话,则直接释放读锁成功,若没有加读锁就释放读锁则抛出异常;,写锁被读锁阻塞等待的场景下,会将readerWait的值进行递减,readerWait表示阻塞写操作goroutine的读操作goroutine数量,当readerWait减到0时则可以唤醒被阻塞写操作的goroutine了;,写锁复用了mutex互斥锁的能力,首先尝试获取互斥锁,获取互斥锁失败就会进入自旋/休眠;,获取互斥锁成功并不代表写锁加锁成功,此时如果还有占用读锁的goroutine,那么就会阻塞住,否则就会加写锁成功,释放写锁会将负值的readerCount变成正值,解除对读锁的互斥,唤醒当前阻塞住的所有读锁,释放互斥锁,读写锁的代码量不多,因为其复用了互斥锁的设计,针对读写锁的功能多做了一些工作,理解起来比互斥锁要容易很多,你学会了吗?

© 版权声明

相关文章