互斥锁和读写锁
面对并发问题,我们始终应该优先考虑使用信道,如果通过信道解决不了的,不得不使用共享内存来实现并发编程的,那 Golang 中的锁机制,就是你绕不过的知识点了。
在 Golang 里有专门的方法来实现锁,还是上一节里介绍的 sync 包。
这个包有两个很重要的锁类型
一个叫 Mutex, 利用它可以实现互斥锁。
一个叫 RWMutex,利用它可以实现读写锁。
互斥锁 :Mutex
使用互斥锁(Mutex,全称 mutual exclusion)是为了来保护一个资源不会因为并发操作而引起冲突导致数据不准确。
举个例子,就像下面这段代码,我开启了三个协程,每个协程分别往 count 这个变量加1000次 1,理论上看,最终的 count 值应试为 3000
func add(count *int, wg *sync.WaitGroup){
for i:= 0; i < 1000; i++{
*count = *count + 1
}
wg.Done()
}
func main() {
var wg sync.WaitGroup
count := 0
wg.Add(3)
go add(&count, &wg)
go add(&count, &wg)
go add(&count, &wg)
wg.Wait()
/*
count的值为: 2078
count的值为: 2516
count的值为: 2079
*/
fmt.Println("count的值为:",count)
}
可运行多次的结果,都不相同
count的值为: 2078
count的值为: 2516
count的值为: 2079
原因就在于这三个协程在执行时,先读取 count 再更新 count 的值,而这个过程并不具备原子性,所以导致了数据的不准确。
解决这个问题的方法,就是给 add 这个函数加上 Mutex 互斥锁,要求同一时刻,仅能有一个协程能对 count 操作。
在写代码前,先了解一下 Mutex 锁的两种定义方法
// 第一种
var lock *sync.Mutex
lock = new(sync.Mutex)
// 第二种
lock := &sync.Mutex{}
利用互斥锁修改上面的代码,得:
func add(count *int, wg *sync.WaitGroup, lock *sync.Mutex){
for i:= 0; i < 1000; i++{
lock.Lock()
*count = *count + 1
lock.Unlock()
}
wg.Done()
}
func main() {
var wg sync.WaitGroup
lock := &sync.Mutex{}
count := 0
wg.Add(3)
go add(&count, &wg, lock)
go add(&count, &wg, lock)
go add(&count, &wg, lock)
wg.Wait()
fmt.Println("count的值为:",count)
}
此时,不管你执行多少次,输出都只有一个结果
count 的值为: 3000
使用 Mutex 锁的注意事项:
-
同一协程里,不要在尚未解锁时再次使加锁
-
同一协程里,不要对已解锁的锁再次解锁
-
加了锁后,别忘了解锁,必要时使用 defer 语句
读写锁:RWMutex
RWMutex,它将程序对资源的访问分为读操作和写操作
为了保证数据的安全,它规定了当有人还在读取数据(即读锁占用)时,不允计有人更新这个数据(即写锁会阻塞)
为了保证程序的效率,多个人(线程)读取数据(拥有读锁)时,互不影响不会造成阻塞,它不会像 Mutex 那样只允许有一个人(线程)读取同一个数据。
读锁允许多个线程同时获得,因为读操作本身是线程安全的。
而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的。
读写锁的特点是:读读不互斥、读写互斥、写写互斥
。
定义一个 RWMuteux 锁,有两种方法
// 第一种
var lock *sync.RWMutex
lock = new(sync.RWMutex)
// 第二种
lock := &sync.RWMutex{}
RWMutex 里提供了两种锁,每种锁分别对应两个方法,为了避免死锁,两个方法应成对出现,必要时请使用 defer。
读锁:调用 RLock 方法开启锁,调用 RUnlock 释放锁
写锁:调用 Lock 方法开启锁,调用 Unlock 释放锁(和 Mutex类似)
基本遵循原则:
-
写锁定情况下,对读写锁进行读锁定或者写锁定,都将阻塞;而且读锁与写锁之间是互斥的;
-
读锁定情况下,对读写锁进行写锁定,将阻塞;加读锁时不会阻塞;
-
对未被写锁定的读写锁进行写解锁,会引发 Panic;
-
对未被读锁定的读写锁进行读解锁的时候也会引发 Panic;
-
写解锁在进行的同时会试图唤醒所有因进行读锁定而被阻塞的 goroutine;
-
读解锁在进行的时候则会试图唤醒一个因进行写锁定而被阻塞的 goroutine。
func main() {
lock := &sync.RWMutex{}
lock.Lock()
for i := 0;i < 4;i++{
go func (i int) {
fmt.Printf("第 %d 个协程准备开始... \n",i)
lock.RLock()
fmt.Printf("第 %d 个协程获得读锁,sleep 1s后,释放锁\n",i)
time.Sleep(time.Second)
lock.RUnlock();
}(i)
}
time.Sleep(time.Second * 2)
fmt.Println("准备释放写锁,读锁不再阻塞")
//写锁一释放,读锁就自由了
lock.Unlock()
lock.Lock()
fmt.Println("程序退出...")
lock.Unlock()
}