Go 怎样用 atomic.Uint64 记录无锁计数
来源:17golang原创
时间:2026-10-18 04:00:11 340浏览 收藏
多个 goroutine 累计已完成事件时,可以把数值放进 atomic.Uint64,用 Add(1) 更新、用 Load() 读取。这里的“无锁计数”指调用代码不再为这一个数显式包一把互斥锁;它不承诺所有平台上的进度特性,也不代表整个业务没有同步成本。
从普通整数改成原子类型,收益先是减少误用
Go 1.19 起提供类型化原子整数。对维护请求累计量、成功事件数的服务而言,把操作收拢到字段的方法上,有助于避免同一个地址一处原子访问、另一处普通读写的混用。这是接口层面的变化,不是本文掌握了某种行业采用率数据。
先限定计数口径:一次 Inc 对应一个已经确定要计入的事件;读取者只需要一个独立数值,不需要同时核对订单状态或另一项余额。若后者必须与计数一致,单独换一个原子字段仍不够。
第一步:给计数器提供窄接口
下面的完整示例要求 Go 1.19 或更高版本。字段不导出,方法使用指针接收者,调用方只能累计和取快照。示例代码未在本文制作过程中执行,后面的数字是按代码逻辑给出的预期。
package main
import (
"fmt"
"sync"
"sync/atomic"
)
// Counter 使用后不能复制,所有调用方共享其指针。
type Counter struct {
n atomic.Uint64
}
// Inc 用一次原子读改写累计事件,返回更新后的值。
func (c *Counter) Inc() uint64 {
return c.n.Add(1)
}
// Value 仅返回该计数器在一次读取时的快照。
func (c *Counter) Value() uint64 {
return c.n.Load()
}
func main() {
c := new(Counter) // 零值可用,不需要另行初始化。
var wg sync.WaitGroup
const workers = 4
const each = 1000
wg.Add(workers)
for i := 0; i
Inc 的返回值属于这一次增量完成时的新值。如果其他 goroutine 紧接着又累计,返回值不会随之改变;因此它不是一个持续更新的引用。当前封装没有暴露清零接口,避免监控消费者把“累计计数”误读成可随时重置的状态量。

第二步:共享对象指针,再区分快照与最终总数
示例中四个任务拿到的是同一个 *Counter,并不是四份计数对象。每个任务累计1000次,等待它们全部结束后,在没有其他写入者且没有回绕的条件下,预期输出为4000。这个最终值既需要原子累计不丢增量,也需要 WaitGroup 把读取放在任务结束之后。
如果把读取放到任务仍在执行的时候,读到的中间数量也可以是正确快照。不能仅凭它小于4000就断言丢计数,更不能用反复读取计数替代任务完成协议。采集器适合读快照,负责收尾的调用方则应等待自己管理的任务结束。
第三步:不要把 Load 加 Store 当成 Add
// 反例:两次原子操作并不组成一次原子加法。
func wrongIncrement(n *atomic.Uint64) {
old := n.Load()
n.Store(old + 1) // 并发调用可能覆盖别人的增量。
}
假设两个调用都读到7,它们可能分别写入8;虽然每次读取、写入都是原子的,两次业务增量仍然只留下一个增量。这里缺少的是一次不可拆开的读改写,而不是给两条语句分别贴上原子标签。正确累计直接使用 Add(1),不要先取出旧值再写回。
同样,先判断计数小于阈值再调用 Add,不能自动实现并发名额限制。本文只做事件累计;涉及条件更新的业务需要另行定义整体同步规则,不把它塞进这个最小计数接口。
第四步:禁止复制与64位对齐要分别处理
已经使用的计数器不能通过结构体赋值、值参数、值接收者或装入按值存储的容器而复制。复制外层结构体也会复制里面的原子字段。需要共享时传指针,需要导出报表时只取 Value() 得到普通 uint64;报表数值与在线计数对象有不同的生命周期。
对齐是另一个维度。在 ARM、386 和32位 MIPS 上,旧式底层64位原子函数要求调用者安排地址的64位对齐;类型化的 atomic.Uint64 则自动满足其对齐要求。正常声明这种字段即可,不要为了“节省字节”把字节缓冲区通过 unsafe 强转成原子计数地址,也不要照抄标准库内部的空结构来冒充编译器识别的对齐机制。

逐步采用时,观察热点而不是预设更快
迁移可以先选一个独立、单调累计的指标,统一入口,再替换读取端;不要只改写入端而保留旧的普通字段读取。若原来一把锁保护多个有关联字段,拆掉它之前必须重新说明这些字段的一致性要求。
高频并发写入同一个数仍可能形成热点,原子操作并不会消除缓存一致性开销。是否值得采用,应在自己的负载上比较吞吐、延迟以及写入竞争;本文不提供未经实测的加速比例,也不展开分片计数方案。
最后要约定数值范围:uint64 的加法按无符号整数规则回绕,不会替你报告业务计数溢出。若导出的累计值允许重启归零、存在清零或回绕可能,消费端不能毫无条件地把相邻快照相减当成新增量。单个字段的原子性也不能自动给多个计数器提供同一时刻的整体快照。
实际采用边界可以压缩为一句话:只统计独立事件,共享同一对象,累计用 Add、取值用 Load;对象身份、任务完成和业务一致性仍由调用方负责。
官方文档入口
类型和方法说明:https://pkg.go.dev/sync/atomic。类型实现与对齐标记说明:https://go.dev/src/sync/atomic/type.go。本文的图片仅帮助对应正文结构,不作为性能或运行结果证据。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
207 收藏
-
203 收藏
-
161 收藏
-
424 收藏
-
283 收藏
-
248 收藏
-
403 收藏
-
209 收藏
-
313 收藏
-
468 收藏
-
460 收藏
-
219 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习