go的读写锁sync.RWMutex
有这样一个经典的读写锁问题,假设读锁和写锁之前互斥,读锁和读锁之间不互斥。现在做一个实验:
1、线程A加一个读锁 ,然后不释放;
2、然后线程B想加一个写锁,会被线程A的读锁阻塞;
3、然后有个线程C尝试去加一个读锁。
按照上面的步骤,步骤3 能加锁成功吗?
使用go语言的sync.RWMutex模拟这段代码,大概如下:
func main() {
var flag int
var rwLock sync.RWMutex
go func() { //sessionA
rwLock.RLock() //加读锁
fmt.Println("session A", "flag=", flag)
}()
go func() { //sessionB
time.Sleep(time.Second * 5)
fmt.Println("sessionB try to get write lock")
rwLock.Lock() //加写锁
flag = 10
fmt.Println("session B", "flag=", flag)
}()
go func() { //sessionC
time.Sleep(time.Second * 8)
fmt.Println("sessionC try to get read lock")
rwLock.RLock() //加读锁
fmt.Println("session C", "flag=", flag)
}()
time.Sleep(time.Second * 15)
fmt.Println("end")
}
执行下上面的代码,我们会发现得出下面这样的结果。
很明显,线程B是没有上锁成功的,因为flag最后的值没有变,还是0;线程C也是没有上锁成功的,因为线程C里面的内容 fmt.Println("session C", "flag=", flag) 没有执行成功。然后问题来了,发现了吗,线程B明明没有上写锁成功啊,为什么我的线程C就是上不了锁啊?毕竟线程A的读锁也不会和我的读锁互斥啊。
下面一段读写锁的概念和图都转载自 小林coding的文章,原文请点击这里
读写锁根据实现的不同,可以分为「读优先锁」和「写优先锁」。
读优先锁期望的是,读锁能被更多的线程持有,以便提高读线程的并发性,它的工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取读锁。如下图:
而写优先锁是优先服务写线程,其工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线程 A 释放读锁后,写线程 B 就可以成功获取读锁。如下图:
看了上面读优先锁和写优先锁的解释,我们可以明白,原来go中sync.RWMutex的实现,是基于写优先原则去实现的,线程C上不了锁就是因为它前面还有个线程B的写锁在等待,而写锁是优先于读锁的。
看了go的sync.RWMutex实现,那么其他的锁也是这么设计的吗?让我们一起去看看MySQL的MDL锁是怎么实现的?
一起做个实验,假设有一张student表长下面这样:
然后我们做下面几个操作:
1、线程A开启事务,然后查询student表,正常返回结果;
2、线程B查询student表,也是正常返回结果;
3、线程C给student表添加字段,被阻塞了。这是因为MySQL中是有MDL(元数据)锁的,当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做结构变更操作的时候,加 MDL 写锁。而写锁和读锁是互斥的,所以这里线程C想获取写锁,但被线程A的读锁阻塞了。
4、线程D再次执行查询语句,发现这时也被阻塞了。
到这里,我们应该也不难发现,MySQL的MDL锁也是写优先锁。其实也很好理解,像上面这个例子,我们在线程C中想加入一个字段s_adress,那么后面的线程D可能就是希望能查出这个新字段的。所以此时线程D被阻塞了,等线程C先执行完。
以上就是从go的读写锁引和MySQL的MDL锁结合在一起,对读写锁的一些思考和研究。我们知道了原来读写锁还有更细的划分 “读优先锁”和“写优先锁”。留个小问题,大家可以去测下MySQL中的行锁,即 FOR UPDATE (写锁)和 LOCK IN SHARE MODE(读锁) ,看下是读优先还是写优先呢?