Day 17|并发安全与数据竞争(race condition)[Go 语言 30 天系统学习计划]

学习目标

  • 理解什么是数据竞争(race condition)
  • 识别 Go 并发中最常见的竞态场景
  • 掌握 Go race detector 的作用与使用方式
  • 学会写出并发安全(race-free)的 Go 代码

一、什么是数据竞争(Race Condition)

在 Go 中,数据竞争指的是:

多个 goroutine 在没有正确同步的情况下,同时访问同一份共享数据,且至少有一个是写操作。

这是一类未定义行为,其结果可能包括:

  • 读到脏数据
  • 状态不一致
  • 程序行为随机、不可复现
  • 线上偶发、难以定位的 bug

工程共识:只要存在数据竞争,程序就是不正确的。


二、最典型的数据竞争示例

示例:

var count int

func main() {
    for i := 0; i < 2; i++ {
        go func() {
            count++
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(count)
}

问题点:

  • count++ 不是原子操作
  • 多个 goroutine 同时读写 count

可能结果:

  • 1
  • 2

工程结论:只要你看到共享变量 + goroutine,就要立刻警惕。


三、数据竞争为什么如此危险

数据竞争的危险之处在于:

  • 本地可能完全复现不了
  • 线上偶发出现
  • 加日志后“消失”(时序被改变)

核心原因:

  • CPU 指令重排
  • 缓存一致性
  • 调度时机不可预测

这也是为什么:“看起来没问题”的并发代码,依然是错的。


四、Go 的 race detector(必须会用)

Go 内置了数据竞争检测器。

启用方式:

go test -race ./...

或:

go run -race main.go

当检测到竞态时,会输出:

  • 冲突的读写位置
  • 对应的 goroutine 栈信息

工程建议:

  • 开发阶段经常开启 -race
  • CI 中至少跑一次 race 测试

五、常见的数据竞争场景

场景 1:未加锁的共享变量

var x int

go func() { x = 1 }()
go func() { fmt.Println(x) }()

场景 2:并发读写 map

m := make(map[string]int)

go func() { m["a"] = 1 }()
go func() { _ = m["a"] }()

结果:可能直接 panic。

场景 3:slice 并发 append

s := []int{}

go func() { s = append(s, 1) }()
go func() { s = append(s, 2) }()

结果:数据错乱或丢失。


六、解决数据竞争的三种基本方式

方式一:使用 mutex 保护共享数据

var (
    mu sync.Mutex
    x  int
)

mu.Lock()
x++
mu.Unlock()

适用场景:

  • 共享状态
  • 频繁读写

方式二:使用 channel 串行化访问

ch := make(chan int)

go func() {
    for v := range ch {
        fmt.Println(v)
    }
}()

ch <- 1
ch <- 2

适用场景:

  • 事件流
  • 任务协作

方式三:避免共享(最理想)

设计原则:

  • 数据只属于一个 goroutine
  • 通过消息传递结果

工程共识:不共享,就没有竞争。


七、原子操作(sync/atomic)的定位

Go 提供了 sync/atomic 包:

atomic.AddInt64(&counter, 1)

适用场景:

  • 简单计数器
  • 状态标志位

注意:

  • 原子操作不等于“复杂逻辑安全”
  • 多个原子操作组合,依然可能有竞态

八、并发安全的工程设计原则

  • 共享数据一定要有“唯一保护方式”
  • 不要混用 mutex 和 channel 保护同一数据
  • 优先选择简单、可读的同步方式
  • 通过设计避免共享,而不是事后补锁

工程结论:并发安全首先是设计问题,其次才是工具问题。


九、并发安全总结

  • 数据竞争是 Go 并发中最危险的问题之一
  • race detector 是你的第一道防线
  • mutex、channel、atomic 各有适用边界
  • “不共享”是终极解决方案

一句话总结:

并发不可怕,失控的并发才可怕。


预告:Day 18|sync/atomic 与无锁编程基础

下一天将系统讲解:

  • atomic 的使用场景
  • 原子操作与内存可见性
  • 为什么“无锁”并不一定更快
  • 工程中如何正确使用 atomic

发表评论