在并发编程中,锁是一种常用的同步机制,用来保护共享资源的安全访问和修改。Golang 作为一门支持并发的语言,提供了两种主要的锁类型:互斥锁(Mutex)和读写锁(RWMutex)。本文将介绍这两种锁的概念、用法和注意事项。
互斥锁
互斥锁是最简单也最常见的一种锁,它用来实现对共享资源的排他访问,即同一时刻只能有一个 goroutine 获得该锁,其他 goroutine 只能等待,直到该锁被释放。Golang 中的互斥锁由 sync.Mutex
结构体表示,它有两个方法:Lock()
和 Unlock()
。Lock()
方法用来加锁,Unlock()
方法用来解锁。使用互斥锁的一般模式如下:
var mu sync.Mutex // 创建一个互斥锁
mu.Lock() // 加锁
// do something with shared resource
mu.Unlock() // 解锁
互斥锁的使用需要注意以下几点:
- 不要重复加锁或解锁,否则会导致运行时错误或死锁。
- 不要忘记解锁,否则会导致其他 goroutine 阻塞,可以使用
defer
语句来确保解锁。 - 不要在多个函数之间传递互斥锁,否则会增加复杂度和出错的风险,建议将互斥锁和共享资源封装在同一个结构体中,提供加锁和解锁的方法。
互斥锁适用于读写不确定或者写多读少的场景,例如,对一个计数器的增加操作就需要用互斥锁来保证原子性和一致性。下面是一个使用互斥锁的例子:
package mainimport ("fmt""sync"
)var (count intmu sync.Mutex
)func main() {for i := 0; i < 10; i++ {go func() {for j := 0; j < 10000; j++ {mu.Lock() // 加锁count++ // 修改共享资源mu.Unlock() // 解锁}}()}// 等待所有 goroutine 结束var wg sync.WaitGroupwg.Add(1)go func() {for {mu.Lock() // 加锁c := count // 读取共享资源mu.Unlock() // 解锁if c >= 100000 {break}}wg.Done()}()wg.Wait()fmt.Println(count) // 输出 100000
}
读写锁
读写锁是一种更复杂的锁,它用来实现对共享资源的读写分离,即允许多个 goroutine 同时读取资源,但只允许一个 goroutine 写入资源,从而提高了并发性能。Golang 中的读写锁由 sync.RWMutex
结构体表示,它有四个方法:RLock()
,RUnlock()
,Lock()
和 Unlock()
。RLock()
和 RUnlock()
用来加解读锁,Lock()
和 Unlock()
用来加解写锁。使用读写锁的一般模式如下:
var rw sync.RWMutex // 创建一个读写锁
rw.RLock() // 加读锁
// do something with shared resource for reading
rw.RUnlock() // 解读锁
rw.Lock() // 加写锁
// do something with shared resource for writing
rw.Unlock() // 解写锁
读写锁的使用需要注意以下几点:
- 不要同时加读锁和写锁,否则会导致死锁。
- 不要重复加锁或解锁,否则会导致运行时错误或死锁。
- 不要忘记解锁,否则会导致其他 goroutine 阻塞,可以使用
defer
语句来确保解锁。 - 不要在多个函数之间传递读写锁,否则会增加复杂度和出错的风险,建议将读写锁和共享资源封装在同一个结构体中,提供加锁和解锁的方法。
读写锁适用于读多写少的场景,例如,对一个缓存的查询操作就可以用读写锁来提高效率。下面是一个使用读写锁的例子:
package mainimport ("fmt""math/rand""sync""time"
)var (cache = make(map[int]int) // 缓存rw sync.RWMutex // 读写锁
)func main() {// 模拟多个 goroutine 同时读写缓存for i := 0; i < 10; i++ {go func() {for {key := rand.Intn(5)rw.RLock() // 加读锁value := cache[key] // 从缓存中读取数据rw.RUnlock() // 解读锁fmt.Printf("goroutine %d read key %d value %d\n", i, key, value)time.Sleep(time.Millisecond * 10)}}()}for i := 0; i < 2; i++ {go func() {for {key := rand.Intn(5)value := rand.Intn(100)rw.Lock() // 加写锁cache[key] = value // 向缓存中写入数据rw.Unlock() // 解写锁fmt.Printf("goroutine %d write key %d value %d\n", i+10, key, value)time.Sleep(time.Millisecond * 100)}}()}// 主 goroutine 等待select {}
}
总结
本文介绍了 Golang 中的两种锁类型:互斥锁和读写锁,以及它们的概念、用法和注意事项。互斥锁用来实现对共享资源的排他访问,适用于读写不确定或者写多读少的场景。读写锁用来实现对共享资源的读写分离,适用于读多写少的场景。在使用锁的时候,要注意避免重复加锁或解锁、忘记解锁、死锁等问题,以及尽量减少锁的粒度和范围,提高并发性能。