Go 互斥锁:sync.Mutex 与 sync.RWMutex
并发编程中,多个协程同时访问共享数据会导致数据竞争。Go 语言提供了 sync.Mutex 和 sync.RWMutex 两种锁机制来解决这个问题。以下指南将直接展示如何选择和使用这两种锁。
1. 基础互斥锁:sync.Mutex
sync.Mutex 是最基础的锁,它就像一个单间的洗手间,任何时刻只允许一个人使用。
使用步骤
- 定义 锁变量。
- 访问 共享资源前,调用
mu.Lock()。 - 操作 完成后,调用
mu.Unlock()释放锁。 - 搭配
defer语句,确保锁在任何情况下(包括发生错误时)都能被释放。
代码示例
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
val int
}
func (c *Counter) Increment() {
// 加锁
c.mu.Lock()
// 使用 defer 确保函数退出时解锁
defer c.mu.Unlock()
// 临界区:安全操作共享数据
c.val++
}
func main() {
var wg sync.WaitGroup
c := Counter{val: 0}
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Increment()
}()
}
wg.Wait()
fmt.Println("Final Value:", c.val)
}
2. 读写锁:sync.RWMutex
sync.RWMutex 区分了读操作和写操作。它允许多个协程同时读取数据,但在写入数据时会阻止所有其他读写操作。适用于“读多写少”的场景。
核心逻辑
以下流程展示了读写锁的决策逻辑:
graph TD
Start["开始: 请求访问资源"] --> Check{操作类型}
Check -- "写操作" --> WriteLock["调用: Lock"]
Check -- "读操作" --> ReadLock["调用: RLock"]
WriteLock --> WriteCrit["独占临界区: 阻塞其他所有读写"]
ReadLock --> ReadCrit["共享临界区: 仅阻塞写操作"]
WriteCrit --> WriteUnlock["调用: Unlock"]
ReadCrit --> ReadUnlock["调用: RUnlock"]
WriteUnlock --> End["结束"]
ReadUnlock --> End
使用步骤
- 定义
sync.RWMutex变量。 - 执行 读操作时,调用
mu.RLock(),结束后 调用mu.RUnlock()。 - 执行 写操作时,调用
mu.Lock(),结束后 调用mu.Unlock()。
代码示例
package main
import (
"fmt"
"sync"
"time"
)
type DataStore struct {
rwmu sync.RWMutex
data map[string]string
}
func (ds *DataStore) Read(key string) string {
// 加读锁:允许并发读,阻塞写
ds.rwmu.RLock()
defer ds.rwmu.RUnlock()
// 模拟耗时读操作
time.Sleep(10 * time.Millisecond)
return ds.data[key]
}
func (ds *DataStore) Write(key, value string) {
// 加写锁:阻塞所有读写
ds.rwmu.Lock()
defer ds.rwmu.Unlock()
// 模拟耗时写操作
time.Sleep(20 * time.Millisecond)
ds.data[key] = value
}
func main() {
ds := DataStore{data: make(map[string]string)}
var wg sync.WaitGroup
// 启动 10 个读协程
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Reader %d: %s\n", id, ds.Read("test"))
}(i)
}
// 启动 1 个写协程
wg.Add(1)
go func() {
defer wg.Done()
ds.Write("test", "new_value")
}()
wg.Wait()
}
3. 对比与选择:Mutex 还是 RWMutex?
根据实际业务场景选择合适的锁。以下是两者的核心区别:
| 特性 | sync.Mutex | sync.RWMutex |
|---|---|---|
| 读写关系 | 读写完全互斥,串行执行 | 读读并发,读写互斥,写写互斥 |
| 性能开销 | 开销较低,逻辑简单 | 读操作虽并发,但锁内部维护状态有额外开销 |
| 适用场景 | 写操作频繁,或读写频率均衡 | 读操作非常频繁,写操作极少 |
4. 关键注意事项
在实际开发中,必须严格遵守以下规则以避免 Bug 和死锁。
禁止复制锁
sync.Mutex 和 sync.RWMutex 包含内部状态字段。如果将其作为值变量复制,内部状态会丢失,导致锁失效或死机。
- 传递 锁时,必须使用 指针
*sync.Mutex。 - 结构体 中若有锁字段,必须通过 指针接收者方法来操作。
// 错误示例:锁被复制
type BadStruct struct {
mu sync.Mutex
}
// 正确示例:使用指针
type GoodStruct struct {
mu sync.Mutex
}
func (g *GoodStruct) SafeMethod() {
g.mu.Lock()
defer g.mu.Unlock()
}
避免重入
Go 的锁是“非重入锁”。同一个协程不能连续多次锁定同一个未解锁的锁,否则会永久阻塞(死锁)。
- 检查 代码逻辑,确保没有递归调用锁。
- 避免 在持有锁时调用外部可能再次尝试获取该锁的函数。
锁的粒度
锁的范围(临界区)越大,并发性能越差。
- 锁定 仅保护真正的共享数据操作。
- 将 耗时的非共享操作(如网络请求、日志打印)移出 锁的范围。
// 优化示例
func Process() {
mu.Lock()
data := sharedData // 仅快速读取共享数据
mu.Unlock() // 尽早解锁
// 执行耗时操作,此时不持有锁,其他协程可以获取锁
result := heavyCalculation(data)
mu.Lock()
sharedData = result // 再次加锁写入
mu.Unlock()
}
暂无评论,快来抢沙发吧!