Go语言通过 go 关键字可以很方便的创建 goroutine 实现并发编程,但是不同的 goroutine 同时访问或者修改公共资源时会带来一些意想不到的结果。在并发编程时,可以借助 Golang 的锁机制来保证数据安全,本文将介绍 Golang 的两种锁:互斥锁 与读写锁 ,首先对这两种锁进行概念上的对比,然后通过实验对比不加锁与加锁的区别,最后介绍读写锁的读锁与写锁的互斥原则。
互斥锁与读写锁的区别 Go 语言标准库 sync
提供了 2 种锁,互斥锁 sync.Mutex
和读写锁 sync.RWMutex
。他们的区别如下:
互斥锁:互斥即不可同时运行。即使用了互斥锁的两个代码片段互相排斥,只有其中一个代码片段执行完成后,另一个才能执行。Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:
读写锁:读写锁分为读锁和写锁,读锁是允许同时执行的,但写锁是互斥的。Go 标准库中提供了 sync.RWMutex 互斥锁类型及其四个方法:
Lock 加写锁
Unlock 释放写锁
RLock 加读锁
RUnlock 释放读锁
不加锁与加互斥锁的区别 下面的代码,开启了三个协程,每个协程让一个公共变量 count 加 10000 次 1,初看下来,最终的 count 值应该是 30000
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "fmt" "sync" ) func main () { count := 0 wg := sync.WaitGroup{} wg.Add(3 ) for i := 0 ; i < 3 ; i++ { go func () { defer wg.Done() for j := 0 ; j < 10000 ; j++ { count = count + 1 } }() } wg.Wait() fmt.Printf("count=%d\n" , count) }
但是实际上运行的结果可能并不等于 30000,而且每次运行的结果各不相同
1 2 3 4 5 6 7 8 count=19623 count=15833 count=20865
原因就在于这三个协程在执行时,先读取 count 再更新 count 的值,而这个过程并不具备原子性,所以导致了数据的不准确。
解决这个问题的方法,就是给修改 count 的代码加上 Mutex 互斥锁,要求同一时刻,仅能有一个协程能对 count 操作。修改上面的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "fmt" "sync" ) func main () { count := 0 lock := sync.Mutex{} wg := sync.WaitGroup{} wg.Add(3 ) for i := 0 ; i < 3 ; i++ { go func () { defer wg.Done() for j := 0 ; j < 10000 ; j++ { lock.Lock() count = count + 1 lock.Unlock() } }() } wg.Wait() fmt.Printf("count=%d\n" , count) }
这样,不管执行多少次上面的程序,输出都只有一个结果:
使用 Mutext 锁虽然很简单,但仍然有几点需要注意:
同一协程里,不要在尚未解锁时再次使加锁
同一协程里,不要对已解锁的锁再次解锁
加了锁后,别忘了解锁,必要时使用 defer 语句
读锁与写锁的互斥原则 主要的互斥原则如下:
读锁之间不互斥,没有写锁的情况下,读锁是无阻塞的,多个协程可以同时获得读锁。
写锁之间是互斥的,存在写锁,其他写锁阻塞。
写锁与读锁是互斥的,如果存在读锁,写锁阻塞,如果存在写锁,读锁阻塞。
先来看一下下面的程序,有四个协程,有两个加写锁,还有两个加读锁,加锁之后分别休眠 1s,表示处理逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package mainimport ( "fmt" "sync" "time" ) var ( lock = sync.RWMutex{} wg = sync.WaitGroup{} ) func writeLock (s string ) { defer wg.Done() lock.Lock() fmt.Printf("goroutine %s get write lock\n" , s) time.Sleep(1 *time.Second) fmt.Printf("goroutine %s release write lock\n" , s) lock.Unlock() } func readLock (s string ) { defer wg.Done() lock.RLock() fmt.Printf("goroutine %s get read lock\n" , s) time.Sleep(1 *time.Second) fmt.Printf("goroutine %s release read lock\n" , s) lock.RUnlock() } func main () { wg.Add(4 ) go writeLock("A" ) go writeLock("B" ) go readLock("C" ) go readLock("D" ) wg.Wait() }
程序的运行结果如下:
1 2 3 4 5 6 7 8 goroutine A get write lock // 协程A获得写锁之后,其他协程必须等它释放写锁之后才能加锁 ——验证了写锁与其他的锁互斥 goroutine A release write lock goroutine D get read lock // 协程C与D在A释放写锁之后,同时获得了读锁,处理逻辑可以并行执行 ——验证了读锁之间不互斥 goroutine C get read lock goroutine D release read lock goroutine C release read lock goroutine B get write lock // 协程B必须要等C和D都释放读锁之后,才能加写锁 ——验证了读锁与写锁互斥,存在读锁,写锁阻塞 goroutine B release write lock