Go多线程(协程)中互斥锁Mutex的使用

前言

多线程操作切片

在 Go 语言中,可以通过 goroutine 来实现多线程操作同一个切片。

但需要注意的是,在多个 goroutine 中同时操作同一个切片时,可能会出现竞争条件(race condition),因此需要使用互斥锁(Mutex)来保护共享资源,以避免数据竞争问题。

主要使用Mutex实现

1
2
3
4
5
var mutex sync.Mutex

//多线程操作的时候
mutex.Lock()
defer mutex.Unlock()

以下是一个示例代码,演示如何在多个 goroutine 中操作同一个切片并使用互斥锁保护:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
"sync"
)

func main() {
var slice []int
var mutex sync.Mutex

var wg sync.WaitGroup
wg.Add(numGoroutines)

for i := 0; i < 5; i++ {
go func() {
mutex.Lock()
defer mutex.Unlock()

// 操作共享切片
for j := 0; j < 3; j++ {
slice = append(slice, j)
}

fmt.Println("Goroutine done")
wg.Done()
}()
}

wg.Wait()

fmt.Println("Final slice:", slice)
}

在上面的示例中,我们定义了一个切片 slice 和一个互斥锁 mutex,然后启动了 5 个 goroutine 并发地向切片中追加数据。

在每个 goroutine 中对切片的修改都需要先获取互斥锁,然后在操作完成后释放互斥锁。

最后等待所有 goroutine 执行完成后输出最终的切片内容。

sync.Mutex 和 sync.RWMutex 的区别

sync.Mutex(互斥锁)

  • 只有一种锁Lock() / Unlock()
  • 同一时刻:最多 1 个 goroutine 能进临界区,其它都要等。
  • 读和写一视同仁:不管你是只读还是修改,进去都要抢同一把锁。
  • 实现简单:不容易在“读/写用错锁类型”上搞混。

sync.RWMutex(读写锁)

  • 两种锁
    • 读锁RLock() / RUnlock() —— 适合只读共享数据。
    • 写锁Lock() / Unlock() —— 适合修改共享数据。
  • 同一时刻
    • 可以 多个 goroutine 同时持读锁(并发读)。
    • 写锁仍是独占:写的时候不能有别的读锁或写锁。
  • 代价:比 Mutex 稍重一点(实现更复杂),但在读多写少时能减少阻塞、提高吞吐。

怎么选(实用规则)

  • 读远多于写,且读路径里绝不改共享结构 → 倾向 RWMutex(读用 RLock,写用 Lock)。
  • 读写差不多,或临界区很短、逻辑简单 → Mutex 往往够用,也好维护。
  • 不能在读锁保护下做写操作;也不要在读锁里再试图“升级”成写锁(容易死锁,要重新设计锁粒度或复制数据再写)。

一句话:Mutex 是“谁进谁独占”;RWMutex 是“多人可以同时读,但写的时候独占”。

RWMutex读写锁

对比

Lock() / Unlock()(写锁)

  • 用途:保护“会修改共享数据”的代码段(写 map、改 slice、更新字段等)。
  • 特性:同一时刻只允许 1 个 goroutine 持有写锁;并且写锁持有期间,所有读锁/写锁都会被阻塞

RLock() / RUnlock()(读锁)

  • 用途:保护“只读共享数据”的代码段(只遍历、只查找、不改)。
  • 特性:同一时刻允许多个 goroutine 同时持有读锁;但如果有人要 Lock()(写锁),新的读锁会被阻塞,写锁会等所有读锁释放后才能拿到。
1
var stateMu sync.RWMutex

关键约束(容易踩坑)

  • 拿了 RLock 就不能在锁内写共享数据(会造成数据竞争,甚至引发 map 并发崩溃)。

  • 不要“读锁升级写锁”:比如先 RLock()Lock(),容易死锁/卡住。

    需要写就直接 Lock(),或先 RUnlock()Lock()(中间状态可能变化,要能接受)。

  • 必须成对释放LockUnlockRLockRUnlock,不要混用。