
本文深入剖析一个看似无锁、单线程(gomaxprocs=1)环境下仍发生的竞态问题,揭示 go 调度器在 channel 操作点的抢占机制如何引发未同步的读写冲突,并演示 race detector 的正确使用与结果解读。
本文深入剖析一个看似无锁、单线程(gomaxprocs=1)环境下仍发生的竞态问题,揭示 go 调度器在 channel 操作点的抢占机制如何引发未同步的读写冲突,并演示 race detector 的正确使用与结果解读。
该程序表面简单,实则暗藏典型并发陷阱:
package main
import "fmt"
var quit chan int
var glo int
func test() {
fmt.Println(glo) // ⚠️ 非同步读取全局变量
}
func main() {
glo = 0
n := 1000000
quit = make(chan int, n)
go test() // 启动 goroutine,但无任何同步机制
for {
quit <- 1 // 关键:channel send 是调度器的潜在抢占点
glo++ // ⚠️ 主 goroutine 并发写入 glo
}
}核心原因并非“多核并行”,而是 Go 调度器的协作式抢占机制。即使 GOMAXPROCS=1,goroutine 也非严格串行执行——Go 运行时会在某些安全点(如 channel 操作、函数调用、系统调用)主动让出 CPU,以便其他就绪 goroutine 运行。quit 中间、不一致的值。
因此,当 n 较小时(如 10000),test 可能恰好在循环末尾才被调度,输出接近 n;而 n 增大后,调度时机的随机性放大,test 更可能在 glo 被递增若干次后即刻执行,导致输出值显著小于 n 且每次运行结果不同——这正是数据竞争(Data Race)的典型表现。
值得注意的是:go run -race 完全能够检测此问题。若未报错,极可能是运行环境或命令执行有误(如文件路径错误、未重新编译)。正确执行应输出类似以下警告:
==================
WARNING: DATA RACE
Read by goroutine 5:
main.test()
/path/test.go:8 +0x6e
Previous write by main goroutine:
main.main()
/path/test.go:18 +0xfe
Goroutine 5 (running) created at:
main.main()
/path/test.go:15 +0x8f
==================✅ 修复方案(任选其一):
- 显式同步:用 sync.WaitGroup 等待 test 完成后再退出;
- 通信代替共享:通过 channel 将 glo 的最终值传给 test;
- 原子操作:若仅需读取快照,可用 atomic.LoadInt32(&glo)(需将 glo 改为 int32);
- 互斥锁:对 glo 的读写加 sync.Mutex 保护。
? 关键总结:Go 的“单线程”不等于“无并发”。只要存在多个 goroutine 且未同步访问共享内存,无论 GOMAXPROCS 设置如何,竞态条件均可能发生。务必依赖 -race 检测,并遵循“不要通过共享内存来通信,而应通过通信来共享内存”的 Go 并发哲学。










