伪代码声明:请无视一些拼写错误
- mutex实例无需实例化,声明即可使用
func add(){
var mutex sync.Mutex
mutex.Lock()
defer mutex.Unlock()
fmtPrintln("test lock")
}
- mutex在传递给外部使用的时候,需要传指针,不然传的是拷贝,会引起锁失败。并且指针的mutex是一定要实例化过的。
func add() *sync.Mutex{
var m = &sync.Mutex{}
return m
}
- 对同一个锁,进行多次锁,会死锁
func a(){
var mutex sync.Mutex
mutex.Lock()
mutex.Lock() // dead lock
}
- 对一个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])
}()
}
- 抛开业务来理解读写锁,它的本质是:
- 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
}
②怎么做到写的时候不让读,读的时候也不让写
答案也很简单,读写时都加写锁。
- 关于不同锁交叉
不同的锁交叉,是允许的,但是要深入理解了锁的另一个原则,才能用的安全。
假设,有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()
}
到这里,是不是更了解锁了呢~~~~