前言

哈喽,大家好,我是asong。前几天一个读者问我如何使用Go语言实现可重入锁,突然想到Go语言中好像没有这个概念,平常在业务开发中也没有要用到可重入锁的概念,一时懵住了。之前在写java的时候,就会使用到可重入锁,然而写了这么久的Go,却没有使用过,这是怎么回事呢?这一篇文章就带你来解密~

什么是可重入锁

之前写过java的同学对这个概念应该了如指掌,可重入锁又称为递归锁,是指在同一个线程在外层方法获取锁的时候,在进入该线程的内层方法时会自动获取锁,不会因为之前已经获取过还没释放而阻塞。美团技术团队的一篇关于锁的文章当中针对可重入锁进行了举例:

假设现在有多个村民在水井排队打水,有管理员正在看管这口水井,村民在打水时,管理员允许锁和同一个人的多个水桶绑定,这个人用多个水桶打水时,第一个水桶和锁绑定并打完水之后,第二个水桶也可以直接和锁绑定并开始打水,所有的水桶都打完水之后打水人才会将锁还给管理员。这个人的所有打水流程都能够成功执行,后续等待的人也能够打到水。这就是可重入锁。

下图摘自美团技术团队分享的文章:

如果是非可重入锁,,此时管理员只允许锁和同一个人的一个水桶绑定。第一个水桶和锁绑定打完水之后并不会释放锁,导致第二个水桶不能和锁绑定也无法打水。当前线程出现死锁,整个等待队列中的所有线程都无法被唤醒。

下图依旧摘自美团技术团队分享的文章:

Go实现可重入锁

既然我们想自己实现一个可重入锁,那我们就要了解java中可重入锁是如何实现的,查看了ReentrantLock的源码,大致实现思路如下:

ReentrantLock继承了父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0,当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。

总结一下实现一个可重入锁需要这两点:

  • 记住持有锁的线程

  • 统计重入的次数

统计重入的次数很容易实现,接下来我们考虑一下怎么实现记住持有锁的线程?

我们都知道Go语言最大的特色就是从语言层面支持并发,GoroutineGo中最基本的执行单元,每一个Go程序至少有一个Goroutine,主程序也是一个Goroutine,称为主Goroutine,当程序启动时,他会自动创建。每个Goroutine也是有自己唯一的编号,这个编号只有在panic场景下才会看到,Go语言却刻意没有提供获取该编号的接口,官方给出的原因是为了避免滥用。但是我们还是通过一些特殊手段来获取Goroutine ID的,可以使用runtime.Stack函数输出当前栈帧信息,然后解析字符串获取Goroutine ID,具体代码可以参考开源项目 - goid。

因为go语言中的GoroutineGoroutine ID,那么我们就可以通过这个来记住当前的线程,通过这个来判断是否持有锁,就可以了,因此我们可以定义如下结构体:

type ReentrantLock struct {lock *sync.Mutexcond *sync.Condrecursion int32host     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 = idrt.recursion = 1
}

这里逻辑比较简单,大概解释一下:

首先我们获取当前GoroutineID,然后我们添加互斥锁锁住当前代码块,保证并发安全,如果当前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/Golang_Dream/tree/master/code_demo/reentrantLock,欢迎star。

为什么Go语言中没有可重入锁

这问题的答案,我在:https://stackoverflow.com/questions/14670979/recursive-locking-in-go#14671462,这里找到了答案。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 stuffmu.Unlock()
}func g() {... do some stuff ...
}func G() {mu.Lock()g()mu.Unlock()
}

这样不仅避免了死锁,而且还对代码进行了解耦。这样的代码按照作用范围进行了分层,就像金字塔一样,上层调用下层的函数,越往上作用范围越大;各层有自己的锁。

总结:Go语言中完全没有必要使用可重入锁,如果我们发现我们的代码要使用到可重入锁了,那一定是我们写的代码有问题了,请检查代码结构,修改他!!!

总结

这篇文章我们知道了什么是可重入锁,并用Go语言实现了可重入锁,大家只需要知道这个概念就好了,实际开发中根本不需要。最后还是建议大家没事多思考一下自己的代码结构,好的代码都是经过深思熟虑的,最后希望大家都能写出漂亮的代码。

好啦,这篇文章到此结束啦,素质三连(分享、点赞、在看)都是笔者持续创作更多优质内容的动力!我是asong,我们下期见。

创建了一个Golang学习交流群,欢迎各位大佬们踊跃入群,我们一起学习交流。入群方式:关注公众号获取。更多学习资料请到公众号领取。

推荐往期文章:

  • Go看源码必会知识之unsafe包

  • Go语言中new和make你使用哪个来分配内存?

  • 源码剖析panic与recover,看不懂你打我好了!

  • 空结构体引发的大型打脸现场

  • Leaf—Segment分布式ID生成系统(Golang实现版本)

  • 面试官:两个nil比较结果是什么?

  • 面试官:你能用Go写段代码判断当前系统的存储方式吗?

  • 如何平滑切换线上Elasticsearch索引

Go语言如何实现可重入锁?相关推荐

  1. c语言线程锁的原理开锁原理图,C++多线程之可重入锁

    #include #include #include using namespace std; recursive_mutex re; void task1() { re.lock(); cout & ...

  2. 【并发编程】线程锁--Synchronized、ReentrantLock(可重入锁)

    在说锁之前,我们要明白为什么要加锁,不加锁会怎样? 在并发编程中,很容易出现线程安全问题,接下来我们看个很经典的例子--银行取钱,来看一下有关线程安全的问题. 取钱的流程可以分为一下几个步骤: 1.用 ...

  3. Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等...

    http://blog.51cto.com/13919357/2339446 Java 中15种锁的介绍 在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类.介绍的内容 ...

  4. 可重入锁ReentrantLock--转载

    突然被问到什么是可重入锁?脑袋里闪过了n中概念,最终没有找到,从网上学习一下. 原文地址:https://www.ibm.com/developerworks/cn/java/j-jtp10264/ ...

  5. 线程调度、公平锁和非公平锁、乐观锁和悲观锁、锁优化、重入锁

    1. 线程调度 线程调度指的就是给线程分配使用处理器的过程.主要的调度方式有两种:协同式调度和抢占式调度. 1.1 协同式调度 线程完成自己的任务之后主动通知系统切换到另一个线程上. 优点: 实现简单 ...

  6. java 变量锁_并发编程高频面试题:可重入锁+线程池+内存模型等(含答案)

    对于一个Java程序员而言,能否熟练掌握并发编程是判断他优秀与否的重要标准之一.因为并发编程是Java语言中最为晦涩的知识点,它涉及操作系统.内存.CPU.编程语言等多方面的基础能力,更为考验一个程序 ...

  7. Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等

    Java 中15种锁的介绍 在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类.介绍的内容如下: 公平锁 / 非公平锁 可重入锁 / 不可重入锁 独享锁 / 共享锁 互 ...

  8. 【Java 并发编程】多线程、线程同步、死锁、线程间通信(生产者消费者模型)、可重入锁、线程池

    并发编程(Concurrent Programming) 进程(Process).线程(Thread).线程的串行 多线程 多线程的原理 多线程的优缺点 Java并发编程 默认线程 开启新线程 `Ru ...

  9. 年轻人,看看 Redisson 分布式锁—可重入锁吧!太重要了

    作者 | 李祥    责编 | 张文 来源 | 企鹅杏仁技术站(ID:xingren-tech) 引言 作为后端开发,对于所谓的线程安全.高并发等一系列名词肯定都不会陌生,相关的一些概念及技术框架是面 ...

最新文章

  1. 统一沟通-技巧-7-Lync 2010-配置信息-EWS未部署-增加版
  2. 工程能力提升管理之道
  3. SetConsoleCtrlHandler 处理控制台消息
  4. java截取指定字符串中的某段字符
  5. 全球最抠门的商场,一年卖了1000亿
  6. 汇编语言将正负数复制到不同的数组
  7. Nginx-详解其原理
  8. STM32应用文件系统--W25Q256(RTT系统)
  9. 绕过卡巴斯基dump进程lsass.exe内存
  10. 如何系统磁盘和raid卡的槽位对应起来
  11. 桌面在计算机哪个文件,电脑桌面上的文件在C盘哪个文件里面
  12. centos5部署open***
  13. 高考作文题“幸存者偏差”难哭了?这有份标准答案
  14. Qt中pri文件介绍及使用(图文超级简单!!)
  15. Volar - vue终极开发神器!
  16. 8步文献综述指南——肯特大学(翻译)
  17. Excel自动化办公(一) | 满足你对Excel数据的所有幻想,python-office一键生成模拟数据
  18. 如何获取UEFI开源资源?
  19. 华为鸿蒙系统专利申请,华为“鸿蒙”系统进一步推进,UI界面获得专利,谷歌慌了!...
  20. Excel数据表格“”连字符4种在使用中的小技巧

热门文章

  1. 【js】js数据类型
  2. 保护森林从这里开始-FSC认证
  3. amd ryzen7 1700 linux,AMD 锐龙 7
  4. 冲上热搜!华为30岁以下员工仅占28%
  5. Gaussian中HOMO和LUMO
  6. 洛谷 P1137 旅行计划 题解
  7. 周末动物园旅游计划书
  8. java获取当前日期(JAVA获取当前日期的前三天工作日)
  9. NoVNC—以Web方式交付VNC远程连接
  10. python数据存储方式_python各数据存储方法