关于golang:Go语言如何实现可重入锁

55次阅读

共计 3835 个字符,预计需要花费 10 分钟才能阅读完成。

原文链接:Go 语言如何实现可重入锁?

前言

哈喽,大家好,我是 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.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
}

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

首先咱们获取以后 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/…

为什么 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 梦工厂] 获取。更多学习材料请到公众号支付。

举荐往期文章:

  • Go 看源码必会常识之 unsafe 包
  • Go 语言中 new 和 make 你应用哪个来分配内存?
  • 源码分析 panic 与 recover,看不懂你打我好了!
  • 空构造体引发的大型打脸现场
  • Leaf—Segment 分布式 ID 生成零碎(Golang 实现版本)
  • 面试官:两个 nil 比拟后果是什么?
  • 面试官:你能用 Go 写段代码判断以后零碎的存储形式吗?
  • 如何平滑切换线上 Elasticsearch 索引

正文完
 0