介绍
Golang 语言天生支持并发,关于并发编程,Golang 语言还有一句口号:“不要通过共享内存进行通信;而是通过通信共享内存”。
但是,通过“共享内存进行通信”的方式作为并发编程的解决方案在传统的编程语言中更为流行。在 Golang 语言标准库 sync 包中也提供了“通过共享内存进行通信”的并发编程解决方案。
其中,在 sync 包中最重要的同步工具就是 sync.Mutex
和 sync.RWMutex
。因为在之前的文章已经介绍过二者的使用,所以本文我们不再赘述。本文主要介绍使用二者的注意事项和二者的区别。
Mutex
Mutex 也称为互斥锁,互斥锁就是互相排斥的锁,它可以用作保护临界区的共享资源,保证同一时刻只有一个 goroutine 操作临界区中的共享资源。互斥锁 Mutex
类型有两个方法,Lock
和 Unlock
。
使用互斥锁的注意事项:
- Mutex 类型变量的零值是一个未锁定状态的互斥锁。
- Mutex 在首次被使用之后就不能再被拷贝(Mutex 是值类型,拷贝会同时拷贝互斥锁的状态)。
- Mutex 在未锁定状态(还未锁定或已被解锁),调用
Unlock
方法,将会引发运行时错误。 - Mutex 的锁定状态与特定 goroutine 没有关联,Mutex 被一个 goroutine 锁定, 可以被另外一个 goroutine 解锁。(不建议使用,必须使用时需要格外小心。)
- Mutex 的
Lock
方法和Unlock
方法要成对使用,不要忘记将锁定的互斥锁解锁,一般做法是使用 defer。
互斥锁源码:
type Mutex struct { state int32 // 互斥锁的状态 sema uint32 // 信号量,用于控制互斥锁的状态 }
03
RWMutex
RWMutex 也称为读写互斥锁,读写互斥锁就是读取/写入互相排斥的锁。它可以由任意数量的读取操作的 goroutine 或单个写入操作的 goroutine 持有。读写互斥锁 RWMutex
类型有五个方法,Lock
,Unlock
,Rlock
,RUnlock
和 RLocker
。其中,RLocker 返回一个 Locker 接口,该接口通过调用 rw.RLock
和 rw.RUnlock
来实现 Lock 和 Unlock 方法。
使用读写互斥锁的注意事项:
- RWMutex 类型变量的零值是一个未锁定状态的互斥锁。
- RWMutex 在首次被使用之后就不能再被拷贝。
- RWMutex 的读锁或写锁在未锁定状态,解锁操作都会引发 panic。
- RWMutex 的一个写锁
Lock
去锁定临界区的共享资源,如果临界区的共享资源已被(读锁或写锁)锁定,这个写锁操作的 goroutine 将被阻塞直到解锁。 - RWMutex 的读锁不要用于递归调用,比较容易产生死锁。
- RWMutex 的锁定状态与特定的 goroutine 没有关联。一个 goroutine 可以 RLock(Lock),另一个 goroutine 可以 RUnlock(Unlock)。
- 写锁被解锁后,所有因操作锁定读锁而被阻塞的 goroutine 会被唤醒,并都可以成功锁定读锁。
- 读锁被解锁后,在没有被其他读锁锁定的前提下,所有因操作锁定写锁而被阻塞的 goroutine,其中等待时间最长的一个 goroutine 会被唤醒。
读写互斥锁源码:
type RWMutex struct { w Mutex // held if there are pending writers writerSem uint32 // semaphore for writers to wait for completing readers readerSem uint32 // semaphore for readers to wait for completing writers readerCount int32 // number of pending readers readerWait int32 // number of departing readers }
04
Mutex 和 RWMutex 的区别
RWMutex 和 Mutex 的区别是 RWMutex 将对临界区的共享资源的读写操作做了区分,RWMutex 可以针对读写操作做不同级别的锁保护。
RWMutex 读写锁中包含读锁和写锁,它的 Lock
和 Unlock
方法用作写锁保护,它的 RLock
和 RUnlock
方法用作读锁保护。
RWMutex 读写锁中的读锁和写锁关系如下:
- 在写锁处于锁定状态时,操作锁定读锁的 goroutine 会被阻塞。
- 在写锁处于锁定状态时,操作锁定写锁的 goroutine 会被阻塞。
- 在读锁处于锁定状态时,操作锁定写锁的 goroutine 会被阻塞。
但是,在读锁处于锁定状态时,操作锁定读锁的 goroutine 不会被阻塞。我们可以理解为读锁保护的临界区的共享资源,多个读操作可以同时执行。
05
总结
本文我们介绍了 Golang 语言中的基本同步原语互斥锁和读写互斥锁使用时的注意事项,然后总结了二者的区别。读写互斥锁可以对临界区的共享资源做更加细粒度的访问控制,在读锁持有锁时,其他操作读锁的 goroutine 不被被阻塞,(也就是说不限制对临界区的共享资源的并发读)所以在读多写少的场景,我们可以使用读写互斥锁替代互斥锁,提升应用程序的性能。
推荐阅读:
参考资料:
https://golang.org/doc/effective_go#concurrency
https://golang.org/pkg/sync/#Mutex
https://golang.org/pkg/sync/#RWMutex