原文连接:Go语言如何实现可重入锁?html
哈喽,你们好,我是asong
。前几天一个读者问我如何使用Go
语言实现可重入锁,忽然想到Go
语言中好像没有这个概念,日常在业务开发中也没有要用到可重入锁的概念,一时懵住了。以前在写java
的时候,就会使用到可重入锁,然而写了这么久的Go
,却没有使用过,这是怎么回事呢?这一篇文章就带你来解密~
以前写过java
的同窗对这个概念应该了如指掌,可重入锁又称为递归锁,是指在同一个线程在外层方法获取锁的时候,在进入该线程的内层方法时会自动获取锁,不会由于以前已经获取过还没释放而阻塞。美团技术团队的一篇关于锁的文章当中针对可重入锁进行了举例:java
假设如今有多个村民在水井排队打水,有管理员正在看管这口水井,村民在打水时,管理员容许锁和同一我的的多个水桶绑定,这我的用多个水桶打水时,第一个水桶和锁绑定并打完水以后,第二个水桶也能够直接和锁绑定并开始打水,全部的水桶都打完水以后打水人才会将锁还给管理员。这我的的全部打水流程都可以成功执行,后续等待的人也可以打到水。这就是可重入锁。git
下图摘自美团技术团队分享的文章:github
若是是非可重入锁,,此时管理员只容许锁和同一我的的一个水桶绑定。第一个水桶和锁绑定打完水以后并不会释放锁,致使第二个水桶不能和锁绑定也没法打水。当前线程出现死锁,整个等待队列中的全部线程都没法被唤醒。面试
下图依旧摘自美团技术团队分享的文章:安全
Go
实现可重入锁既然咱们想本身实现一个可重入锁,那咱们就要了解java
中可重入锁是如何实现的,查看了ReentrantLock
的源码,大体实现思路以下:并发
ReentrantLock
继承了父类AQS
,其父类AQS
中维护了一个同步状态status
来计数重入次数,status
初始值为0
,当线程尝试获取锁时,可重入锁先尝试获取并更新status
值,若是status == 0
表示没有其余线程在执行同步代码,则把status
置为1
,当前线程开始执行。若是status != 0
,则判断当前线程是不是获取到这个锁的线程,若是是的话执行status+1
,且当前线程能够再次获取锁。释放锁时,可重入锁一样先获取当前status
的值,在当前线程是持有锁的线程的前提下。若是status-1 == 0
,则表示当前线程全部重复获取锁的操做都已经执行完毕,而后该线程才会真正释放锁。分布式
总结一下实现一个可重入锁须要这两点:函数
统计重入的次数很容易实现,接下来咱们考虑一下怎么实现记住持有锁的线程?学习
咱们都知道Go
语言最大的特点就是从语言层面支持并发,Goroutine
是Go
中最基本的执行单元,每个Go
程序至少有一个Goroutine
,主程序也是一个Goroutine
,称为主Goroutine
,当程序启动时,他会自动建立。每一个Goroutine
也是有本身惟一的编号,这个编号只有在panic
场景下才会看到,Go语言
却刻意没有提供获取该编号的接口,官方给出的缘由是为了不滥用。可是咱们仍是经过一些特殊手段来获取Goroutine ID
的,可使用runtime.Stack
函数输出当前栈帧信息,而后解析字符串获取Goroutine ID
,具体代码能够参考开源项目 - goid。
由于go
语言中的Goroutine
有Goroutine ID
,那么咱们就能够经过这个来记住当前的线程,经过这个来判断是否持有锁,就能够了,所以咱们能够定义以下结构体:
type ReentrantLock struct { lock *sync.Mutex cond *sync.Cond recursion int32 host int64 }
其实就是包装了Mutex
锁,使用host
字段记录当前持有锁的goroutine id
,使用recursion
字段记录当前goroutine
的重入次数。这里有一个特别要说明的就是sync.Cond
,使用Cond
的目的是,当多个Goroutine
使用相同的可重入锁时,经过cond
能够对多个协程进行协调,若是有其余协程正在占用锁,则当前协程进行阻塞,直到其余协程调用释放锁。具体sync.Cond
的使用你们能够参考我以前的一篇文章:源码剖析sync.cond(条件变量的实现机制)。
func NewReentrantLock() sync.Locker{ res := &ReentrantLock{ lock: new(sync.Mutex), recursion: 0, host: 0, } res.cond = sync.NewCond(res.lock) return res }
Lock
func (rt *ReentrantLock) Lock() { id := GetGoroutineID() rt.lock.Lock() defer rt.lock.Unlock() if rt.host == id{ rt.recursion++ return } for rt.recursion != 0{ rt.cond.Wait() } rt.host = id rt.recursion = 1 }
这里逻辑比较简单,大概解释一下:
首先咱们获取当前Goroutine
的ID
,而后咱们添加互斥锁锁住当前代码块,保证并发安全,若是当前Goroutine
正在占用锁,则增长resutsion
的值,记录当前线程加锁的数量,而后返回便可。若是当前Goroutine
没有占用锁,则判断当前可重入锁是否被其余Goroutine
占用,若是有其余Goroutine
正在占用可重入锁,则调用cond.wait
方法进行阻塞,直到其余协程释放锁。
Unlock
func (rt *ReentrantLock) Unlock() { rt.lock.Lock() defer rt.lock.Unlock() if rt.recursion == 0 || rt.host != GetGoroutineID() { panic(fmt.Sprintf("the wrong call host: (%d); current_id: %d; recursion: %d", rt.host,GetGoroutineID(),rt.recursion)) } rt.recursion-- if rt.recursion == 0{ rt.cond.Signal() } }
大概解释以下:
首先咱们添加互斥锁锁住当前代码块,保证并发安全,释放可重入锁时,若是非持有锁的Goroutine
释放锁则会致使程序出现panic
,这个通常是因为用户用法错误致使的。若是当前Goroutine
释放了锁,则调用cond.Signal
唤醒其余协程。
测试例子就不在这里贴了,代码已上传github
:https://github.com/asong2020/...
Go
语言中没有互斥锁这问题的答案,我在:https://stackoverflow.com/que...Go
语言的发明者认为,若是当你的代码须要重入锁时,那就说明你的代码有问题了,咱们正常写代码时,从入口函数开始,执行的层次都是一层层往下的,若是有一个锁须要共享给几个函数,那么就在调用这几个函数的上面,直接加上互斥锁就行了,不须要在每个函数里面都添加锁,再去释放锁。
举个例子,假设咱们如今一段这样的代码:
func F() { mu.Lock() //... do some stuff ... G() //... do some more stuff ... mu.Unlock() } func G() { mu.Lock() //... do some stuff ... mu.Unlock() }
函数F()
和G()
使用了相同的互斥锁,而且都在各自函数内部进行了加锁,这要使用就会出现死锁,使用可重入锁能够解决这个问题,可是更好的方法是改变咱们的代码结构,咱们进行分解代码,以下:
func call(){ F() G() } func F() { mu.Lock() ... do some stuff mu.Unlock() } func g() { ... do some stuff ... } func G() { mu.Lock() g() mu.Unlock() }
这样不只避免了死锁,并且还对代码进行了解耦。这样的代码按照做用范围进行了分层,就像金字塔同样,上层调用下层的函数,越往上做用范围越大;各层有本身的锁。
总结:Go
语言中彻底没有必要使用可重入锁,若是咱们发现咱们的代码要使用到可重入锁了,那必定是咱们写的代码有问题了,请检查代码结构,修改他!!!
这篇文章咱们知道了什么是可重入锁,并用Go
语言实现了可重入锁,你们只须要知道这个概念就行了,实际开发中根本不须要。最后仍是建议你们没事多思考一下本身的代码结构,好的代码都是通过深思熟虑的,最后但愿你们都能写出漂亮的代码。
好啦,这篇文章到此结束啦,素质三连(分享、点赞、在看)都是笔者持续创做更多优质内容的动力!我是asong
,咱们下期见。
建立了一个Golang学习交流群,欢迎各位大佬们踊跃入群,咱们一块儿学习交流。入群方式:关注公众号[Golang梦工厂]获取。更多学习资料请到公众号领取。
推荐往期文章: