首页 / Go 语言入门教程 / sync.Mutex 与 RWMutex

Go 语言入门教程

sync.Mutex 与 RWMutex

本教程共 80 篇 · 第 62 篇 · 更新于 2026-07-27 · 约 8 分钟阅读

GoGo 入门教程syncMutexRWMutex互斥锁读写锁死锁

62. sync.Mutex 与 RWMutex

本节目标:学会用 sync.Mutex 保护共享数据,掌握读写锁 sync.RWMutex 的使用场景,理解死锁原因和避免方法。

为什么需要锁

上一节讲过数据竞争。多个 goroutine 同时改一个变量会出问题。channel 是通过通信解决,锁是另一种方式—直接把共享数据保护起来,同一时刻只允许一个 goroutine 操作。

sync.Mutex 互斥锁

sync.Mutex 是最基础的锁,只有两个方法:

  • Lock():加锁
  • Unlock():解锁

加锁后,别的 goroutine 再调 Lock 会阻塞,直到锁被释放。

package main

import (
	"fmt"
	"sync"
)

var (
	count int
	mu    sync.Mutex
)

func main() {
	var wg sync.WaitGroup
	wg.Add(1000)

	for i := 0; i < 1000; i++ {
		go func() {
			defer wg.Done()
			mu.Lock()
			count++
			mu.Unlock()
		}()
	}

	wg.Wait()
	fmt.Println(count) // 永远是 1000
}

对比上一节没加锁的版本,这次结果稳定是 1000。

用 defer 解锁

如果加锁和解锁之间有多个 return 路径,容易忘了解锁。习惯写法是用 defer

func update() {
	mu.Lock()
	defer mu.Unlock()

	count++
	if count > 100 {
		return // 不会忘解锁,defer 兜底
	}
	count += 10
}
Tip

defer 会在函数返回时自动解锁。但要注意 defer 也有微小开销,在极高性能要求的循环里可以手动 Unlock。大多数场景 defer 是首选,安全第一。

互斥锁不可重入

Go 的 Mutex 不是递归锁(不可重入)。同一个 goroutine 重复 Lock 会死锁:

func a() {
	mu.Lock()
	defer mu.Unlock()
	b() // b 里又 Lock,死锁!
}

func b() {
	mu.Lock()
	defer mu.Unlock()
	fmt.Println("b")
}

a 持有锁后调 bb 又想拿同一把锁,永远等不到。

Warning

Go 故意不做可重入锁。如果你发现自己需要重入,多半是设计有问题。考虑把锁的范围拆小,或者把需要锁的逻辑拆成不带锁的内部函数。

sync.RWMutex 读写锁

如果读多写少,用 sync.Mutex 会让读操作也互相排队,浪费。sync.RWMutex 解决这个问题:

  • 多个 goroutine 可以同时持有读锁
  • 写锁是排他的,和任何读锁、写锁互斥
var (
	data  map[string]string
	rwmu  sync.RWMutex
)

func read(key string) string {
	rwmu.RLock()         // 读锁
	defer rwmu.RUnlock()
	return data[key]
}

func write(key, val string) {
	rwmu.Lock()          // 写锁
	defer rwmu.Unlock()
	data[key] = val
}

读写锁适合「读远多于写」的场景,比如配置缓存、查询服务。

RWMutex 的方法

rwmu.RLock()      // 加读锁
rwmu.RUnlock()    // 解读锁
rwmu.Lock()       // 加写锁
rwmu.Unlock()     // 解写锁

还有两个非阻塞的尝试方法(Go 1.18+):

if rwmu.TryRLock() {  // 尝试加读锁,失败返回 false 不阻塞
	// 拿到了
	rwmu.RUnlock()
}

if rwmu.TryLock() {   // 尝试加写锁
	// 拿到了
	rwmu.Unlock()
}
Note

TryLock 只在特殊场景用,比如你想抢锁但抢不到就跳过。正常逻辑还是用阻塞式的 Lock

读锁别滥用

读写锁不是万金油。如果你的读写比例差不多,RWMutex 反而比 Mutex 慢,因为管理读写锁状态本身有开销。经验值:读占比超过 80% 才值得用读写锁。

另外,持有读锁时别做耗时操作,否则会拖慢想拿写锁的 goroutine。

死锁的常见原因

死锁就是 goroutine 之间互相等,谁都不让步。常见几种:

1. 重复加锁(前面讲过)

2. 锁顺序不一致

两个 goroutine 各自拿了不同的锁,又想拿对方的锁:

// goroutine A: lock1 -> lock2
// goroutine B: lock2 -> lock1
// 互相等,死锁

解决办法:所有地方按相同顺序加锁。

3. 等 channel 却没人发

func main() {
	ch := make(chan int)
	mu.Lock()
	ch <- 1    // 阻塞,但锁还拿着
	mu.Unlock()
}
Warning

持有锁时不要做可能阻塞的操作(channel 收发、IO、sleep)。锁的临界区要尽量短,只保护共享数据的读写。

锁要传指针

sync.Mutex 是结构体,复制后是另一把锁,保护不了同一个数据:

// 错误:复制了锁
func bad(wg sync.WaitGroup) { // 应该传 *sync.WaitGroup
	defer wg.Done()
}

sync.Mutexsync.WaitGroup 等都包含内部状态,必须用指针传递或作为结构体的指针字段。

Tip

可以用 go vet 检查锁复制的问题,它会报 assignment copies lock value 之类的警告。

小结

  • sync.Mutex:互斥锁,Lock/Unlock 保护临界区
  • sync.RWMutex:读写锁,读多写少场景用,RLock/RUnlock 共享读
  • 习惯用 defer Unlock,避免忘解锁
  • Go 锁不可重入,重复加锁会死锁
  • 锁顺序要一致,临界区要短,持锁别阻塞
  • 锁必须传指针,不能复制

下一节讲 sync.WaitGroupsync.Once