文章目录

Go语言 原子操作Atomic在计数器中的应用

发布于 2026-04-08 06:27:57 · 浏览 10 次 · 评论 0 条

Go语言 原子操作Atomic在计数器中的应用

在并发编程中,当多个 Goroutine 同时读写同一个变量时,会引发数据竞争。为了解决这个问题,通常可以使用互斥锁或原子操作。原子操作由底层硬件支持,执行过程不可被中断,因此在处理计数器等简单数值累加场景时,性能远优于互斥锁。


1. 复现并发安全问题

首先,创建一个存在并发风险的计数器程序,观察数据丢失的现象。

  1. 定义一个整型变量 count,初始值为 0。
  2. 开启 10,000 个 Goroutine。
  3. 在每个 Goroutine 中,执行 count++ 操作。
  4. 等待所有 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. 使用互斥锁解决问题

互斥锁是一种常见的解决方案,它保证同一时间只有一个协程能访问变量。

  1. 引入 sync 包。
  2. 声明一个 sync.Mutex 类型的变量 mu
  3. 在修改变量前,调用 mu.Lock()
  4. 在修改变量后,调用 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 指令直接完成,效率更高。

  1. 引入 sync/atomic 包。
  2. 将变量类型从 int 修改int64(atomic 函数对类型有严格要求)。
  3. 使用 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 的处理逻辑:

graph TD A["读取内存值 V"] --> B{"V == 预期值 E?"} B -- "是" --> C["写入新值 New_V"] B -- "否" --> A C --> D["操作成功"]

在数学上,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

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文