线程安全
概念说明
当多个 goroutine 同时访问同一份共享数据时,如果至少有一个 goroutine 在写数据,就可能产生数据竞争。
数据竞争会让结果不稳定,也可能让程序在运行时出现难以复现的问题。
线程安全的目标是:
多个并发任务同时执行时,共享数据仍然保持正确状态。
语法/规则
- 并发读取同一份不可变数据通常是安全的。
- 并发读写或并发写同一个变量时,需要同步保护。
- 简单数值计数可以使用
sync/atomic。 - 复杂临界区通常使用
sync.Mutex。 - 可以使用
go test -race或go run -race检测数据竞争。
原子计数示例
| |
输出结果:
| |
这里的 defer wg.Done() 怎么理解
先看这行代码:
| |
在这个示例里,可以先把它理解成:
| |
也就是说,它不是一写到这里就立刻执行。
而是先把 wg.Done() 记下来,等当前 goroutine 里的代码快执行完时再调用。
所以这段 goroutine 更接近下面这种理解:
| |
只是写成 defer wg.Done() 更安全。
因为即使后面 goroutine 里的代码变多了,Done() 也更不容易漏写。
这两个包在这里是做什么的
示例里导入了这两个包:
| |
在这篇里可以先这样理解:
sync是 Go 标准库里处理并发同步的包。- 这篇示例里,
sync只用到了WaitGroup。 WaitGroup的作用是:等待前面启动的所有 goroutine 都执行完成。
对应到代码里就是:
| |
而 sync/atomic 也可以先这样理解:
sync/atomic是 Go 标准库里提供原子操作的包。- 这篇示例里,
sync/atomic只用到了atomic.AddInt64(&count, 1)。 - 它的作用是:安全地把
count加 1,避免多个 goroutine 同时修改时出现数据竞争。
对应到代码里就是:
| |
可以先把它理解成:
| |
所以在这个示例中:
sync.WaitGroup负责等所有 goroutine 做完。sync/atomic负责让多个 goroutine 安全地修改同一个计数器。
常见错误
- 认为
count++是一个原子操作,实际它包含读取、计算、写回多个步骤。 - 只在写入时加锁,读取时不加锁,仍可能产生数据竞争。
- 不使用
-race检测并发代码,导致问题只在压力场景中暴露。