Golang中sync.RWMutex 读写锁的实现
作者:FfHUCisI
本文主要介绍了RWMutex如何通过允许多个读操作同时进行、仅在写入时互斥,解决Mutex的读写全互斥瓶颈,具有一定的参考价值,感兴趣的可以了解一下
一、读写锁解决了什么问题
普通的 Mutex 不管操作是读还是写,一律互斥。但很多场景下,读操作远多于写操作,读操作之间本可以安全并发——它们不会修改数据。
RWMutex 提供两种锁:
- 读锁(RLock):多个 goroutine 可以同时持有,彼此不阻塞
- 写锁(Lock):独占,持有期间所有读锁和写锁都被阻塞
这一设计显著提升了读多写少场景的并发性能。
二、基本用法
var (
mu sync.RWMutex
balance int64
)
// 读操作:使用读锁
func Balance() int64 {
mu.RLock()
defer mu.RUnlock()
return balance
}
// 写操作:使用写锁(和 Mutex 一样)
func Deposit(amount int64) {
mu.Lock()
defer mu.Unlock()
balance += amount
}
命名约定:Lock/Unlock 用于写,RLock/RUnlock 用于读。
三、锁的互斥规则
| 已有读锁 | 已有写锁 | |
|---|---|---|
| 请求读锁 | ✅ 允许 | ❌ 阻塞 |
| 请求写锁 | ❌ 阻塞 | ❌ 阻塞 |
核心理解:读读不互斥,其他都互斥。
四、适用场景判断
RWMutex 不是万能的。它的内部维护了更复杂的记账信息,无竞争时比 Mutex 稍慢。只有在以下条件同时满足时,RWMutex 才有优势:
- 读操作占绝大多数
- 存在锁竞争(多个 goroutine 同时在等锁)
如果没有竞争,Mutex 更轻更快;如果写操作频繁,RWMutex 不但无益,还会因为写锁不断阻塞读锁而导致性能下降。
五、读锁不能升级为写锁
这是一个经典的坑:你不能先 RLock,然后在持有读锁的情况下再 Lock。
mu.RLock() // ... 发现需要修改数据 ... mu.Lock() // 死锁!写锁在等待读锁释放,读锁永远不会释放
正确的做法是先 RUnlock,再 Lock,并且重新检查条件(因为释放读锁到获取写锁之间,数据可能已被其他 goroutine 改变):
mu.RLock()
if needUpdate {
mu.RUnlock()
mu.Lock()
// 重新检查!可能在等写锁期间别人已经改过了
if needUpdate { /* 执行更新 */ }
mu.Unlock()
} else {
mu.RUnlock()
}
六、不要假设"只读"就一定安全
即使一个方法看起来只读取数据,它也可能悄悄修改内部状态(比如更新访问计数器、填充缓存)。如果不确定,用 Mutex 更安全。
七、练习代码
保存为 rwmutex_demo.go:
package main
import (
"fmt"
"sync"
"time"
)
// 一个简单的配置中心:读多写少
type Config struct {
mu sync.RWMutex
data map[string]string
}
func NewConfig() *Config {
return &Config{
data: map[string]string{"version": "1.0", "env": "dev"},
}
}
// 读配置(允许并发读)
func (c *Config) Get(key string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.data[key]
}
// 写配置(独占)
func (c *Config) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
// 升级演示:先读后决定是否写
func (c *Config) SetIfChanged(key, newValue string) bool {
c.mu.RLock()
oldValue := c.data[key]
c.mu.RUnlock()
if oldValue == newValue {
return false // 值相同,不写
}
// 获取写锁,重新检查
c.mu.Lock()
defer c.mu.Unlock()
// 重新检查:等写锁期间可能已被别人修改
if c.data[key] == newValue {
return false
}
c.data[key] = newValue
return true
}
func main() {
cfg := NewConfig()
var wg sync.WaitGroup
// 模拟 100 个并发读
for i := 0; i < 100; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
v := cfg.Get("version")
_ = v // 实际使用
}(i)
}
// 模拟 5 个并发写
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
cfg.Set(fmt.Sprintf("key_%d", id), fmt.Sprintf("val_%d", id))
}(i)
}
// 模拟升级锁模式
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
cfg.SetIfChanged("version", "1.0")
}(i)
}
wg.Wait()
c.mu.RLock()
fmt.Printf("配置条目数: %d\n", len(c.data))
fmt.Printf("version: %s\n", c.data["version"])
c.mu.RUnlock()
}
运行结果:
配置条目数: 7
version: 1.0
八、关键要点
| 要点 | 说明 |
|---|---|
| RLock/RUnlock | 读锁,允许多个 goroutine 同时持有 |
| Lock/Unlock | 写锁,独占,阻塞所有读写 |
| 不可升级 | 不能从读锁升级为写锁,必须先释放再获取 |
| 重新检查 | 释放读锁到获取写锁之间数据可能变化,需 double-check |
| 适用场景 | 读多写少 + 存在锁竞争 |
| 无竞争时 | Mutex 比 RWMutex 更轻更快 |
到此这篇关于Golang中sync.RWMutex 读写锁的实现的文章就介绍到这了,更多相关Golang sync.RWMutex 读写锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
