当前位置:首页>java>《你的 Go 代码真的线程安全吗?90% 的人都忽略了这一点》

《你的 Go 代码真的线程安全吗?90% 的人都忽略了这一点》

  • 2026-08-26 05:00:54
《你的 Go 代码真的线程安全吗?90% 的人都忽略了这一点》
你以为你写的代码会按顺序执行?在多核 CPU 下,你的直觉可能正在出卖你。

先问自己:你真的懂内存模型吗?

来看这段代码,你觉得最后会打印什么?

var x intvar done bool// goroutine Agofunc() {    x = 1    done = true}()// goroutine B(轮询等待)for !done {}fmt.Println(x)  // 你觉得一定会打印 1?

答案是:你不能确定打印 1。甚至在某些情况下,这个 for 循环可能永远不会退出。

为什么?这就是内存模型要解决的问题。


内存模型是什么?

一句话定义:

内存模型,就是语言对你承诺:在并发场景下,一个 goroutine 写入的数据,在什么条件下能被另一个 goroutine 观察到。

听起来很简单,但它背后涉及三个现实:

现实一:CPU 有多核,每个核有自己的缓存(Cache)

        ┌─────────┐        ┌─────────┐        │  Core A  │        │  Core B  │        │  Cache A │        │  Cache B │        └────┬─────┘        └────┬─────┘             │                   │        ┌────▼───────────────────▼────┐        │         主内存 (RAM)         │        └─────────────────────────────┘

Core A 写入的数据,可能先在 Cache A 里,还没同步到主内存,Core B 就看不到。

现实二:编译器会重排指令

你写的顺序:

写 x → 写 done

编译器可能优化为:

写 done → 写 x

现实三:CPU 本身也会重排执行顺序

即使编译器没动,硬件层面也可能乱序执行指令以提高吞吐。

这三个现实加在一起,就是为什么开头那段代码不可靠。


Go 内存模型的核心思想:Happens-Before

Go 官方文档给出了一个核心概念来解决上面的混乱:

如果事件 A “happens before” 事件 B,那么 A 的写入对 B 是可见的。

简单来说就是:你必须用语言提供的同步机制,来建立明确的 happens-before 关系,否则你无法保证数据对其他 goroutine 可见。

没有 happens-before 关系 = 行为未定义 = 你的代码有潜在的 bug。


哪些东西能建立 Happens-Before?

Go 给你提供了几种工具,每种都能建立确定的 happens-before 关系:

1. Channel(信道)

发送 → happens before → 对应的接收完成信道关闭 → happens before → 接收到零值
var x intch := make(chanstruct{})gofunc() {    x = 1// ①写 x    ch <- struct{}{}  // ②发送}()<-ch                // ③接收(happens after ②)fmt.Println(x)      // ④读 x →一定能看到 1 ✓

发送发生在接收之前,而写 x 发生在发送之前,所以写 x happens-before 读 x。链条建立,保证了可见性。

2. Mutex(互斥锁)

第 n 次 Unlock → happens before → 第 n+1 次 Lock
var mu sync.Mutexvar x intgofunc() {    mu.Lock()    x = 1    mu.Unlock()  // ① Unlock}()mu.Lock()        // ② Lock(happens after ①)fmt.Println(x)   // 一定能看到 1 ✓mu.Unlock()

3. sync.Once

once.Do(f) 中 f 的执行 → happens before → 任何 once.Do 返回
var once sync.Oncevar x intinitX := func() {    x = 42// 只执行一次}// 无论多少个 goroutine 同时调用,都能安全读到 42gofunc() { once.Do(initX); fmt.Println(x) }()gofunc() { once.Do(initX); fmt.Println(x) }()gofunc() { once.Do(initX); fmt.Println(x) }()

4. atomic(原子操作)

atomic 写入 → happens before → 后续的 atomic 读取(对同一变量)
var x intvar done atomic.Boolgofunc() {    x = 1    done.Store(true)  // atomic 写入}()for !done.Load() {}   // atomic 读取fmt.Println(x)        // 看到 1 ✓

回到开头那段代码,把 done 改成 atomic.Bool 就能修复。


回到开头:为什么那段代码出问题?

var x intvar done bool// ←普通变量,没有任何同步机制gofunc() {    x = 1    done = true// 写入可能被重排到 x = 1 之前}()for !done {}   // 编译器可能把 done 缓存在寄存器里,永远读不到更新fmt.Println(x)

问题所在:

  • done
     是普通变量,没有建立任何 happens-before 关系
  • 编译器可能优化掉循环中对 done 的重复读取
  • 即使 done 变成了 true,x = 1 也不一定对另一个 goroutine 可见

修复:换用 channel 或 atomic

// ✅用 channelch := make(chanstruct{})gofunc() {    x = 1    ch <- struct{}{}}()<-chfmt.Println(x)  // 安全✓// ✅用 atomicvar done atomic.Boolgofunc() {    x = 1    done.Store(true)}()for !done.Load() {}fmt.Println(x)  // 安全✓

一图记住全部

你写的代码的执行顺序        ↓ 可能被编译器重排 + CPU 重排 + 缓存不一致        ↓ 导致goroutine 之间数据不可见        ↓ 解决方案是用 happens-before 机制建立同步        ↓ Go 提供的工具Channel / Mutex / Once / Atomic

最后的忠告

在 Go 中,如果你在多个 goroutine 之间共享变量,你必须使用同步机制。不要依赖”它在我电脑上能正常运行”来判断代码的正确性——那可能只是因为你的机器顺序执行了指令。

记住一个原则:共享变量 + 多个 goroutine = 必须同步。没有例外。


本文参考 Go 官方内存模型文档:https://go.dev/ref/mem

最新文章

随机文章