golang 关于锁 mutex,你可能还需要继续理解

伪代码声明:请无视一些拼写错误
  1. mutex实例无需实例化,声明即可使用
func add(){
    var mutex sync.Mutex
    mutex.Lock()
    defer mutex.Unlock()
    fmtPrintln("test lock")
}
  1. mutex在传递给外部使用的时候,需要传指针,不然传的是拷贝,会引起锁失败。并且指针的mutex是一定要实例化过的。
func add() *sync.Mutex{
    var m = &sync.Mutex{}
    return m
}
  1. 对同一个锁,进行多次锁,会死锁
func a(){
    var mutex sync.Mutex
    mutex.Lock()
    mutex.Lock() // dead lock
}
  1. 对一个RWLock进行同时Lock()和RLock()会死锁.
func a(){
    var mutex sync.RWMutex
    mutex.RLock()
    mutex.Lock() // dead lock
}

这意味着如果一个操作函数里同时包含写和读,千万不要这么写

type Object struct{
    Data []interface{}
    L  sync.RWMutex
}
func WR(o Object){
    o.L.Lock()
    defer o.L.UnLock()
    o.Data = append(o.Data, 1)
    
    o.L.RLock()
    defer o.L.RUnLock()
    fmt.Println(o.Data[len(o.Data)-1])
}

因为defer是在return前执行,该段逻辑的锁顺序实际上是 Lock(), RLock(), UnLock(),RUnLock() 死锁了
可以改成:

func WR(o Object){
    func(){
        o.L.Lock()
        defer o.L.UnLock()
        o.Data = append(o.Data, 1)
    }()

    func(){
        o.L.RLock()
        defer o.L.RUnLock()
        fmt.Println(o.Data[len(o.Data)-1])
    }()
}

  1. 抛开业务来理解读写锁,它的本质是:
  • Lock()时,会阻塞另一个协程Rlock()和Lock()
  • Rlock时,不会阻塞另一个协程Rlock()。但是会阻塞另一个协程的Lock()

读的时候加读锁,写的时候加写锁,只是恰好满足了我们大多数场景的需求,如果要真正理解锁,可以问问自己下面两个问题:

①.假定有一个data,怎么做到,让他在读时,不让写,写时让读?

很简单读取的时候加写锁,写的时候加读锁。

// 因为读取函数的Lock会使得Rlock阻塞,所以就做到了,读取的时候不让写,写的时候时读锁,不影响readData的调用。
var m sync.Mutex
var data = 5
func readData() int {
    m.Lock()
    defer m.Unlock()
    return data
}

func writeData() {
   m.Rlock()
   defer m.RUnlock()
   data =6
}

②怎么做到写的时候不让读,读的时候也不让写
答案也很简单,读写时都加写锁。

  1. 关于不同锁交叉
    不同的锁交叉,是允许的,但是要深入理解了锁的另一个原则,才能用的安全。
    假设,有data1,data2,data3,他们对应有l1,l2, l3 三个读写锁。
    如果,你想要在操作一个变量时,阻塞掉他们三个的读写,你可能会封装一个这样的函数:
var l1,l2,l3 sync.RWMutex
var data1,data2,data3 int
var count int

func Glock() {
    l1.Lock()
    l2.Lock()
    l3.Lock()
}

func GUnlock() {
    l1.Unlock()
    l2.Unlock()
    l3.Unlcok()
}
func CountIncr() {
   Glock()
   count ++
   Gunlock 
}

好了,你想了想,Glock时,确实把data1,data2,data3的读写操作都锁住了,你以为没问题可以交卷了,那就大错特错了。

单纯这一部分代码确实不会死锁,那么,我再提下一个需求,你再操作一下试试。

现在,需要将data1的值,赋予给data3,你理所应当地写道

func F() {
   var tmp int
   l3.lock()
   l1.RLock()
   data3 = data1
   l1.RUnlock()
   l3.Unlock()
}

好了,你想了想,读取data1的值,赋予data3,所以读锁l1,写锁l3,完美。

死锁了!

为什么呢?
Glock里,l3的lock再l1之后,就是说,可能有某一个协程,l1Lock()通了,走到l3时,等待另一个协程的l3 Unlock时,他才能走得通l3.Lock(),也就是,l1在等待l3放锁,他才能走到下面的l1.Unlock(),否则将永久阻塞,走不到l1.Unlock().

而,下面的代码里,l3在lock之后,必须要等l1.Rlock()通行,才能走到后面的l3.Unlock(),只要l1Rlock一直阻塞,将永久阻塞。即l3在等待l1解锁。

那么就必然,有概率,走到一个a协程里,l1在等l3,b协程里l3在等l1。结果你说呢,必将死锁。

现在理解了锁把, 那么思考一下,怎么解决上面的问题呢?

很简单,最笨的方法是,避免锁交叉,将第二份代码里的交叉锁,分开,假设data都是map类型

var tmp = make(map[string]string,0)
l1.RLock()
for k,v:=range data1 {
    tmp[k] = v
}
l1.RUnlock()

l3.Lock()
for k,v:=range tmp {
    data3[k] = v
}
l3.Unlock()

你瞧,这样是不是避免了锁交叉,只要不进行锁交叉,就永远不存在A等B的场景。

但,为什么说这是一个蠢办法呢?
因为,go里,不同的锁对象是允许交叉的。你想想,如果每一次读写,都需要迭代一遍,用一个复制体来避免交叉,这得多浪费迭代的复杂度和空间啊。毕竟map是一个O(1)的结构,被你玩成了O(n)

我们知道,go里经常报cricle import,循环引用,解决方法就是层级关系。package A 作为上层,可以importB, B作为下层,永远不能import上层的额东西。保持规范,就能避免循环引用。

同理,lock交叉也允许,那么我们只需要永远保证,A等待B,而B不能等待A,就不会死锁了
第一步:

func Glock() {
    l1.Lock()
	    l2.Lock()
    		l3.Lock()
}

func GUnlock() {
   			l3.Unlcok()
    	l2.Unlock()
    l1.Unlcok()
}

我们约束,l1是上层锁,允许l1,等待l2,和l3,同时所以放锁顺序,就必须先放l3,再放l2,在放l1

其次,在交叉里,也是l1等l3

func F() {
   var tmp int
   l1.RLock()
	   l3.lock()
		   data3 = data1
	   l3.Unlock()
   l1.RUnlock()
}

到这里,是不是更了解锁了呢~~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值