Go互斥锁和读写锁
之前一直有些搞混Go的锁,本文就来学习下Go的两种锁:互斥锁和读写锁。
Go语言标准库sync包提供了两种锁类型:sync.Mutex
(互斥锁),sync.RWMutex
(读写锁)
互斥锁的场景
以下使用示例来演示不加锁的情况:开启三个goroutine,让每个goroutine都对count加1000次1,这样正常情况下最终的count应该是3000
func main() {
count := 0
g := sync.WaitGroup{}
g.Add(3)
for i := 0; i < 3; i++ {
go func() {
defer g.Done()
for j := 0; j < 1000; j++ {
count = count + 1
}
}()
}
g.Wait()
fmt.Println("count = ", count)
}
执行结果如下图:
执行了三次,结果却不是3000,而且每一次都不一样,很明显这里产生了问题,就是在count加1时,可能刚读取了count,另一个协程又把count给改了,导致数据不准确,比如可能存在以下场景:
时刻 | 协程1 | 协程2 |
---|---|---|
时刻1 | 读取count为0 | 读取count为0,执行count + 1 |
时刻2 | 赋值count = 1 | |
时刻3 | 执行count+1并赋值count = 1 |
这样子就会导致,两个协程都执行了+1,但是结果却还是1,导致最终结果不是3000.
解决方案:加锁
func main() {
count := 0
mu := sync.Mutex{}
g := sync.WaitGroup{}
g.Add(3)
for i := 0; i < 3; i++ {
go func() {
defer g.Done()
for j := 0; j < 1000; j++ {
mu.Lock()
count = count + 1
mu.Unlock()
}
}()
}
g.Wait()
fmt.Println("count = ", count)
}
在count = count + 1
前加锁,使得同一时刻只有一个goroutine能读取到count
读写锁
想象下有这种场景,比如银行存钱取钱时,对余额修改是需要加锁的,因为涉及到修改,同一时刻可能有人给你汇钱,同时你也在取钱,就可能导致数据不准确的问题,所以需要加锁,而且读取账户余额时也是需要等待修改操作结束,才能读取到正确的结果的。
然而大部分情况下,读取的操作会比较频繁,如果读取可以并发执行,那效率就会得到很大的提高了,因此只要保证在并发读取的时候没有写的操作,就不会产生问题。
这就是读写锁,读写锁分为了读锁和写锁,写锁肯定是互斥的,但读锁是允许同时执行的(对应了以上场景)
一般来说,读写锁存在以下几种情况:
- 读锁之间不互斥,没有写锁的情况下,读锁不阻塞
- 写锁之间是互斥的,存在写锁,则其他写锁会阻塞
- 写锁和读锁之间是互斥的,存在写锁,则读锁会阻塞;存在读锁,则写锁会阻塞
在读场景比较多时,使用读写锁能有效减少锁阻塞的时间
读写锁示例
var mu sync.RWMutex
var g sync.WaitGroup
func Write(name string) {
defer g.Done()
mu.Lock()
fmt.Printf("%s get write lock...\n", name)
time.Sleep(1 * time.Second)
fmt.Printf("%s release write lock...\n\n", name)
mu.Unlock()
}
func Read(name string) {
defer g.Done()
mu.RLock()
fmt.Printf("%s get read lock...\n", name)
time.Sleep(1 * time.Second)
fmt.Printf("%s release write lock...\n\n", name)
mu.RUnlock()
}
func main() {
g.Add(4)
go Write("One")
go Write("Two")
go Read("Third")
go Read("Four")
g.Wait()
}
执行结果如下(只是其中一种情况):
通过执行结果可以看出,当第一个协程获取到写锁时,其他所有协程都阻塞等待锁释放,然后第四个协程获取到读锁时(此时没有写锁),其他协程并没有阻塞,所以第三个协程紧跟着也获取到了读锁,然后第三个第四个协程释放了读锁,第二个协程才能获取到写锁(读写锁互斥的)
区别
- 互斥锁:互斥的意思就是不可以同时执行,也就是两个代码段互相排斥,只有一个能执行,另一个必须等到锁释放后才可以拿到锁
sync.Mutex
提供了以下两个方法:
Lock
: 加锁Unlock
: 释放锁
- 读写锁:读写锁分为了读锁和写锁,读锁是运行同时执行的,但写锁是互斥的(很好理解,同一时间多个读取是可以的,但是多个写就不行了,会导致数据错乱)
sync.RWMutex
提供了以下四个方法:
Lock
: 加写锁Unlock
: 释放写锁RLock
: 加读锁RUnlock
: 释放读锁