登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

atomic类型实现无锁计数器

来源:17golang原创

时间:2026-11-15 05:00:08 335浏览 收藏

Go 可以用 atomic.Uint64 的 Add(1) 实现并发计数,再用 Load() 读取总数。业务代码无需给单个数值的递增加互斥锁,但必须共享同一个计数器实例,且不能把一次原子操作误认为整个业务流程都具备原子性。

官方文档:https://pkg.go.dev/sync/atomic

项目目标:统计一组并发任务的完成次数

小项目只做一件事:启动6个 goroutine,每个递增700次,任务全部完成后打印最终总数。这里的“无锁计数器”指计数字段使用原子操作、业务侧不为递增显式持有 Mutex;它不是对整个程序、运行时调度或所有平台底层实现作出的实时性保证。

普通整数的 total++ 包含读取、加一、写回。多个 goroutine 无同步地修改同一整数会发生数据竞争,也可能丢失更新。换成 Load 再 Store 同样不能让这两次调用合成一次不可分割的递增,应直接使用 Add。

环境准备:使用带类型的原子数值

atomic.Uint64 在 Go 1.19 加入,零值即可作为初始为0的计数器。项目不需要第三方依赖,保存为 main.go 后即可用 Go 工具链运行。字段封装进 Counter,外部通过指针接收者方法访问,便于保持同一份状态。

WaitGroup 负责等待任务结束,Counter 负责数值更新。这两个对象有不同职责:Load 能读到一个原子的当前值,却不会等待尚未执行的递增。最终汇总需要明确的任务完成条件。

并发任务、共享Counter和WaitGroup的静态关系
结构说明图:任务共享一个Counter实例,WaitGroup独立维护任务完成关系;连线不是执行时序。

核心代码:递增用 Add,读取用 Load

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

// Counter 使用后不复制,所有调用都共享同一个实例。
type Counter struct {
    total atomic.Uint64
}

func (c *Counter) Inc() {
    c.total.Add(1) // 用一次原子读改写完成递增。
}

func (c *Counter) Value() uint64 {
    return c.total.Load() // 并发读取也经过原子接口。
}

func main() {
    const workers, each = 6, 700
    counter := &Counter{}
    var wg sync.WaitGroup
    wg.Add(workers) // 在启动任务前登记,避免与 Wait 形成错误时序。

    for i := 0; i 

Counter 没有额外构造函数,因为原子字段零值已经可用。Inc 和 Value 都是指针接收者,调用不会复制正在使用的字段。各 goroutine 收到的也是同一个 counter 指针,而不是各自独立的 Counter 值。

wg.Add 在任务启动前登记数量,每个任务用 defer wg.Done 表达完成,主函数 Wait 返回后才打印汇总。示例没有任何真实业务失败分支;接入业务后应明确什么才算“完成”,避免把任务启动、尝试次数和成功次数混在一起。

本地运行:按任务数量推导预期结果

# 在保存 main.go 的目录运行这个独立示例。
go run main.go

# 可用竞态检测器辅助发现实际执行路径上的数据竞争。
go run -race main.go

按代码语义,预期两项数字都是4200,即 total=4200 expected=4200。这是预期结果说明,本文没有把它描述成本机运行日志。竞态检测不能证明所有业务路径都正确,也不能发现“两个原子操作合起来不符合业务约束”这类逻辑问题。

运行中如果提前调用 Value,读到较小值也不一定意味着更新丢失,可能只是其他任务还没有完成。因此结果是否符合预期,需要同时看读取位置和等待条件,不能只看计数字段是否用了 atomic。

部署与集成:避免复制,也别把复合条件拆开

官方要求 Uint64 首次使用后不能复制。包含它的 Counter 同样应按不可复制对象管理:不要用值接收者,不要把运行中的 Counter 按值传参,也不要对含有它的业务结构体随意作值拷贝。对外输出数字快照,用 Value 返回的 uint64,而不是把整个对象复制出去。

如果业务是“当前数量低于上限才加一”,先 Load 判断再 Add 会留下并发窗口,多个任务都可能通过判断。可以为这种单字段条件更新另行设计 CompareAndSwap 重试,或直接使用互斥锁保护完整条件。不要把本文只递增的 Inc 生搬到额度控制上。

两个原子字段分别保存成功数与总数,也不意味着两个 Load 构成一致快照。需要跨字段不变量或同时更新多个对象时,Mutex 往往更容易表达。若多个任务高频争用同一数值,原子操作仍有共享写入成本;是否更快,应按真实负载测量,本文不提供未经测量的倍数。

单次原子更新与复合业务条件的职责边界
结构说明图:Add和Load提供单次操作语义;等待、条件更新与多字段一致性分别需要自己的设计。

长期累加还要考虑 uint64 的范围。溢出不会替你生成业务错误,不能无限期把单个固定宽度计数器当作永不耗尽的账本。需要允许负数的增减场景,可以重新评估 atomic.Int64 与字段范围,而不是绕过类型约束。

验收清单:看状态共享和业务语义

检查项本例做法容易混淆的地方
共享对象传递同一个Counter指针每个任务创建自己的计数器
并发递增Add(1)Load后Store不等于原子递增
读取总数Load()读取不代表等待任务完成
最终结果Wait后汇总提前读取不一定丢失更新
多个字段另行设计同步边界多个原子调用不等于事务

Add 返回什么?返回加法完成后的新值;若需要本次递增得到的序号,可接收这个返回值,仍要考虑溢出和业务唯一性范围。

是否所有地方都该用 atomic?不必。它适合独立数值状态;有复杂条件和关联数据时应选择更清楚的同步方式。

能直接打印Counter作为快照吗?不要依赖对整个含原子字段对象的复制或格式化来表达并发快照,先调用 Value 取得普通整数再输出。

参考:atomic.Uint64 标准库文档;sync.WaitGroup 文档。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>