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 能读到一个原子的当前值,却不会等待尚未执行的递增。最终汇总需要明确的任务完成条件。

核心代码:递增用 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 往往更容易表达。若多个任务高频争用同一数值,原子操作仍有共享写入成本;是否更快,应按真实负载测量,本文不提供未经测量的倍数。

长期累加还要考虑 uint64 的范围。溢出不会替你生成业务错误,不能无限期把单个固定宽度计数器当作永不耗尽的账本。需要允许负数的增减场景,可以重新评估 atomic.Int64 与字段范围,而不是绕过类型约束。
验收清单:看状态共享和业务语义
| 检查项 | 本例做法 | 容易混淆的地方 |
|---|---|---|
| 共享对象 | 传递同一个Counter指针 | 每个任务创建自己的计数器 |
| 并发递增 | Add(1) | Load后Store不等于原子递增 |
| 读取总数 | Load() | 读取不代表等待任务完成 |
| 最终结果 | Wait后汇总 | 提前读取不一定丢失更新 |
| 多个字段 | 另行设计同步边界 | 多个原子调用不等于事务 |
Add 返回什么?返回加法完成后的新值;若需要本次递增得到的序号,可接收这个返回值,仍要考虑溢出和业务唯一性范围。
是否所有地方都该用 atomic?不必。它适合独立数值状态;有复杂条件和关联数据时应选择更清楚的同步方式。
能直接打印Counter作为快照吗?不要依赖对整个含原子字段对象的复制或格式化来表达并发快照,先调用 Value 取得普通整数再输出。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习