Golang学习-sync.RWMutex 读写锁
sync.RWMutex 读写锁
一、读写锁解决了什么问题
普通的 Mutex 不管操作是读还是写,一律互斥。但很多场景下,读操作远多于写操作,读操作之间本可以安全并发——它们不会修改数据。
RWMutex 提供两种锁:
- 读锁(RLock):多个 goroutine 可以同时持有,彼此不阻塞
- 写锁(Lock):独占,持有期间所有读锁和写锁都被阻塞
这一设计显著提升了读多写少场景的并发性能。
二、基本用法
var(mu sync.RWMutex balanceint64)// 读操作:使用读锁funcBalance()int64{mu.RLock()defermu.RUnlock()returnbalance}// 写操作:使用写锁(和 Mutex 一样)funcDeposit(amountint64){mu.Lock()defermu.Unlock()balance+=amount}命名约定:Lock/Unlock 用于写,RLock/RUnlock 用于读。
三、锁的互斥规则
| 已有读锁 | 已有写锁 | |
|---|---|---|
| 请求读锁 | ✅ 允许 | ❌ 阻塞 |
| 请求写锁 | ❌ 阻塞 | ❌ 阻塞 |
核心理解:读读不互斥,其他都互斥。
四、适用场景判断
RWMutex 不是万能的。它的内部维护了更复杂的记账信息,无竞争时比 Mutex 稍慢。只有在以下条件同时满足时,RWMutex 才有优势:
- 读操作占绝大多数
- 存在锁竞争(多个 goroutine 同时在等锁)
如果没有竞争,Mutex 更轻更快;如果写操作频繁,RWMutex 不但无益,还会因为写锁不断阻塞读锁而导致性能下降。
五、读锁不能升级为写锁
这是一个经典的坑:你不能先 RLock,然后在持有读锁的情况下再 Lock。
mu.RLock()// ... 发现需要修改数据 ...mu.Lock()// 死锁!写锁在等待读锁释放,读锁永远不会释放正确的做法是先 RUnlock,再 Lock,并且重新检查条件(因为释放读锁到获取写锁之间,数据可能已被其他 goroutine 改变):
mu.RLock()ifneedUpdate{mu.RUnlock()mu.Lock()// 重新检查!可能在等写锁期间别人已经改过了ifneedUpdate{/* 执行更新 */}mu.Unlock()}else{mu.RUnlock()}六、不要假设"只读"就一定安全
即使一个方法看起来只读取数据,它也可能悄悄修改内部状态(比如更新访问计数器、填充缓存)。如果不确定,用 Mutex 更安全。
七、练习代码
保存为rwmutex_demo.go:
packagemainimport("fmt""sync""time")// 一个简单的配置中心:读多写少typeConfigstruct{mu sync.RWMutex datamap[string]string}funcNewConfig()*Config{return&Config{data:map[string]string{"version":"1.0","env":"dev"},}}// 读配置(允许并发读)func(c*Config)Get(keystring)string{c.mu.RLock()deferc.mu.RUnlock()returnc.data[key]}// 写配置(独占)func(c*Config)Set(key,valuestring){c.mu.Lock()deferc.mu.Unlock()c.data[key]=value}// 升级演示:先读后决定是否写func(c*Config)SetIfChanged(key,newValuestring)bool{c.mu.RLock()oldValue:=c.data[key]c.mu.RUnlock()ifoldValue==newValue{returnfalse// 值相同,不写}// 获取写锁,重新检查c.mu.Lock()deferc.mu.Unlock()// 重新检查:等写锁期间可能已被别人修改ifc.data[key]==newValue{returnfalse}c.data[key]=newValuereturntrue}funcmain(){cfg:=NewConfig()varwg sync.WaitGroup// 模拟 100 个并发读fori:=0;i<100;i++{wg.Add(1)gofunc(idint){deferwg.Done()v:=cfg.Get("version")_=v// 实际使用}(i)}// 模拟 5 个并发写fori:=0;i<5;i++{wg.Add(1)gofunc(idint){deferwg.Done()cfg.Set(fmt.Sprintf("key_%d",id),fmt.Sprintf("val_%d",id))}(i)}// 模拟升级锁模式fori:=0;i<10;i++{wg.Add(1)gofunc(idint){deferwg.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 更轻更快 |
