【Go弃用可重入锁】为什么可重入锁/递归锁的设计“不太好”

挺久前就想写一下了,但因太忙一直鸽着,好几月了,今天给补上

虽然网上有很多文章都有说明一些,但却没完全说透,但这里我再以最简洁的方式总结下,给出点自己看法,以最大化节省读者时间

Java里面可重入锁(递归锁)能解决重量级锁引发的死锁问题,不挺好的吗?比如下面代码(mutex相当于Java重量级锁),对外提供了两个方法F和G,如果是Java的话,调用F,进入F时Lock一次,进入G时又Lock一次,此时会出现死锁情况,相当于线程占用了资源后又自己等自己

func F() {
    mutex.Lock()
  
    preF() // 业务f1
  
    G() // 假设需要用到业务g
  
    postF() // 业务f2
  
    mutex.Unlock()
}

func G() {
    mutex.Lock()
    // 业务g
    mutex.Unlock()
}

如果是可重入锁则不会死锁,进入F时Lock一次锁的可重入次数+1,进入G时又Lock一次又+1,对应Unlock时再-1,正常执行结束。而且可重入锁可在用户进程空间实现,避免了额外开销,又能解决死锁,性能也比重量级锁好,为什么还说其设计“不太好”呢?

可以看到,作为为并发而生的Go语言一直没有引入可重入锁,就是因为Russ Cox大佬坚持可重入锁的设计理念不好,他认为,既然你加了锁,那你就要保证锁住的这部分资源/区域具有“状态不变性”,具有状态不变性的集合称为状态不变量(Russ Cox称为“the invariants”),而可重入锁破坏了这种不变性状态。

状态不变量(“the invariants”)简单理解就是一个同步的范围,或者一个同步的元素集合,显然在上述代码中,如果是可重入锁,直接执行F是没问题,但如果F、G都是对外暴露的方法呢?那么外界在单独调用G的时候,很有可能有其他线程(用户)在调用F,这时,两种不同性质的线程就会用到同一个mutex,这就会出问题。

这里的不同性质的线程是指不同业务的线程,如果多个线程只执行F函数,那么这些线程是相同性质的线程。一个mutex理应当只保护一个同步范围,对同一性质的多个线程进行同步,正如Russ Cox说的:“Recursive mutexes do not protect invariants. Mutexes have only one job, and recursive mutexes don't do it.”(普通mutex保证单一职责,但可重入mutex做不到),所以出问题的原因就在于这里异化了mutex的职责——本来只用保护一块同步范围,现在要保护两块,即mutex出现了两种场景下的语义,混用了语义,当然会容易出错。

有人会问,你这不太对啊,怎么感觉怪怪的,你上面代码mutex换成普通的锁,一样会出问题呀,这和可重入锁有啥关系?

没错,如果F、G都是对外暴露的方法,在多线程环境下这么写确实也会出问题,比如F如果执行过长,会导致本来很快执行完的G超时,这是最简单的情况,属于程序员的低级错误。但问题的本质不在这里,问题的本质在于是否混淆了语义。如果mutex上的是普通锁,那么从宏观角度看,要么是G执行,要么是F执行,两者是互斥的语义,不存在G和F的交集。如果mutex上的是可重入锁,那么从宏观角度看,同样是G执行时,F不能执行,F执行时,G不能执行,但若将视角切细点,从F的角度看,那么会发现,F执行时,G也可以执行,那么我到底听谁的?这就好像你新来的接手一个复杂项目,文档告诉你G和F的调用方式是互斥的,但实际上他又可以不是完全互斥的——其实这就是混淆了mutex的语义。而正是这种看似不起眼的语义混淆,在复杂的代码环境下,就是滋生bug的温床。这也是Russ Cox亲自说的:

“Recursive mutexes are just a mistake, nothing more than a comfortable home for bugs.”

那么如何改?很简单,就像七层网络的分层模型一样,用层与层分离关注点,即用层与层分离语义,比如分离出同步方法和普通方法这两种语义:

func g(){
  // g的业务
}

func f(){
  preF() // 业务f1
  g()
  postF() // 业务f2
}

func F(){
  mu.Lock()
  f()
  mu.Unlock()
}

func G(){
  mu.Lock()
  g()
  mu.Unlock() 
}

可以看到,分离后语义后,G、F用于提供并发环境下的方法,而g、f 则focus业务自身,这样写,代码既解耦,职责清晰,又不需要使用可重入锁,岂不是一举两得?

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值