Golang中的sync包实现了两种锁:互斥锁(Mutex)和读写锁(RWMutex)。
互斥锁(sync.Mutex)
- 使用Lock方法加锁,使用Unlock方法解锁,Golang从1.18新增了TryLock方法,用于尝试获取锁,返回成功或者失败;
- 如果Mutex被一个goroutine获取后,其他goroutine只能等到这个goroutine释放该Mutex后才能获取;
- 使用Lock方法加锁后,不能再次加锁,直到使用Unlock方法释放锁后才能再次加锁;加锁之前使用Unlock方法会导致panic;
- Mutex并不与特定的goroutine相关联,一个goroutine加锁,也可以使用另一个goroutine解锁;
- 在同一个goroutine中的Mutex解锁之前再次进行加锁,会导致死锁。
互斥锁的一个经典案例是对切片进行并发安全的操作,代码如下:
package main
import (
"fmt"
"sync"
)
var mu sync.Mutex
var slice []int
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
for j := 0; j < 100; j++ {
mu.Lock()
slice = append(slice, j)
mu.Unlock()
}
wg.Done()
}()
}
wg.Wait()
fmt.Println(len(slice))
}
首先定义了一个长度为0的slice,并发地对其进行了1000次append操作。
读写锁(RWMutex)
- RWMutex该锁可以加多个读锁(RLock)或者一个写锁(Lock),Golang从1.18新增了TryLock和TryRLock方法,分别用于尝试获取写锁和读锁,返回成功或者失败。
- 读锁被占用的情况下会阻止写,不会阻止读,多个goroutine可以同时获得读锁。
- 写锁会阻止其他goroutine对写锁和读锁的获取。
看一个代码示例:
package main
import (
"log"
"sync"
)
var mu sync.RWMutex
var m map[string]string
func main() {
m = make(map[string]string)
mu.Lock()
m["a"] = "A"
mu.Unlock()
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
mu.RLock()
log.Println(m["a"]) // 并发读
mu.RUnlock()
wg.Done()
}(i)
}
wg.Wait()
}
首先定义了一个map,然后对其进行了一次写操作和10次并发读操作。在并发读的时候,使用读锁来支持并发访问。
小结
本文讲解了互斥锁和读写锁的特点,两种锁都可以用于协调多个并发访问共享资源的同步机制。可以看出互斥锁适用于读写不确定,并且只有一个读或者写的场景;读写锁适用于读多写少的场景,可以有更高的性能。需要注意的是尽量避免在锁的范围内进行耗时较长的操作,以免影响性能。