Go语言 原子操作Atomic在计数器中的应用
在并发编程中,当多个 Goroutine 同时读写同一个变量时,会引发数据竞争。为了解决这个问题,通常可以使用互斥锁或原子操作。原子操作由底层硬件支持,执行过程不可被中断,因此在处理计数器等简单数值累加场景时,性能远优于互斥锁。
1. 复现并发安全问题
首先,创建一个存在并发风险的计数器程序,观察数据丢失的现象。
- 定义一个整型变量
count,初始值为 0。 - 开启 10,000 个 Goroutine。
- 在每个 Goroutine 中,执行
count++操作。 - 等待所有 Goroutine 执行完毕,打印最终结果。
package main
import (
"fmt"
"sync"
)
func main() {
var count int
var wg sync.WaitGroup
// 开启 10000 个协程
for i := 0; i < 10000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
count++ // 非原子操作
}()
}
wg.Wait()
fmt.Println("最终计数:", count)
}
运行上述代码,你会发现每次输出的结果都不同,且几乎总是小于 10000。这是因为 count++ 在底层分为“读取-修改-写入”三步,多个协程并发执行时,部分修改会被覆盖。
2. 使用互斥锁解决问题
互斥锁是一种常见的解决方案,它保证同一时间只有一个协程能访问变量。
- 引入
sync包。 - 声明一个
sync.Mutex类型的变量mu。 - 在修改变量前,调用
mu.Lock()。 - 在修改变量后,调用
mu.Unlock()。
func main() {
var count int
var wg sync.WaitGroup
var mu sync.Mutex
for i := 0; i < 10000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock() // 加锁
count++
mu.Unlock() // 解锁
}()
}
wg.Wait()
fmt.Println("加锁后计数:", count)
}
虽然结果正确,但频繁加锁解锁会导致协程上下文切换,增加系统开销。
3. 使用原子操作优化性能
Go 语言提供了 sync/atomic 包,用于对基本类型进行原子操作。这种方法不需要锁,依靠 CPU 指令直接完成,效率更高。
- 引入
sync/atomic包。 - 将变量类型从
int修改为int64(atomic 函数对类型有严格要求)。 - 使用
atomic.AddInt64()替换count++操作。
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var count int64
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 原子性地加 1
atomic.AddInt64(&count, 1)
}()
}
wg.Wait()
fmt.Println("原子操作计数:", count)
}
注意:atomic.AddInt64 的第一个参数必须是指针类型,这样才能直接修改内存中的值。
4. 原子操作的底层原理:CAS
原子操作的核心机制是 CAS(Compare-And-Swap)。其逻辑是:包含三个参数(内存值 V、旧预期值 A、新值 B)。当且仅当 V 的值等于 A 时,CPU 才会将 V 的值更新为 B,否则什么都不做。
这一过程由硬件保证是原子的。下面的流程图描述了 CAS 的处理逻辑:
在数学上,CAS 操作可以表示为:
$$ CAS(V, E, N) = \begin{cases} V \leftarrow N & \text{if } V == E \\ V & \text{if } V \neq E \end{cases} $$
Go 语言中的 atomic.CompareAndSwapInt64 系列函数即是对此指令的封装。
5. 常用原子操作函数
除了加法,sync/atomic 还提供了读取、存储、交换等功能。
| 函数名 | 功能描述 | 适用场景 |
|---|---|---|
atomic.AddInt64 |
原子加法(支持负数实现减法) | 计数器、生成序列号 |
atomic.LoadInt64 |
原子读取 | 安全地获取当前值 |
atomic.StoreInt64 |
原子写入 | 初始化配置标志位 |
atomic.SwapInt64 |
原子交换并返回旧值 | 交换状态标记 |
atomic.CompareAndSwapInt64 |
原子比较并交换 | 实现无锁队列 |
6. 读取原子变量的正确姿势
当你使用 atomic.AddInt64 修改变量时,不要直接通过变量名读取。
- ❌ 错误做法:直接读取
fmt.Println(count)。这可能导致读取到未完全同步的“脏数据”。 - ✅ 正确做法:使用
atomic.LoadInt64(&count)进行读取。
// 安全地读取原子变量
finalValue := atomic.LoadInt64(&count)
fmt.Println("安全读取的值:", finalValue)
如果原子变量需要在 for 循环中被频繁检查,建议使用 atomic.LoadInt64。
7. 性能对比总结
为了量化差异,我们可以从理论层面对比 Mutex 和 Atomic 的特点。
| 特性 | sync.Mutex | sync/atomic |
|---|---|---|
| 并发模型 | 悲观锁(假设会发生冲突) | 乐观锁(假设不发生冲突) |
| 上下文切换 | 竞争激烈时会发生,有开销 | 无需切换,用户态完成 |
| 适用复杂度 | 适用于保护一段代码逻辑 | 仅适用于单个数值操作 |
| 性能 | 较低(高并发下) | 极高 |
核心结论:
如果仅对整型变量进行加减、读取或赋值操作,首选 sync/atomic。如果需要保护一段复杂的逻辑代码块(例如切片追加、结构体多字段修改),则必须使用 sync.Mutex。

暂无评论,快来抢沙发吧!