atomic原子操作
本篇是你 Go 并发学习的 第 13 天:atomic 包、CPU 占用分析,会对比 Java 里的 AtomicInteger、自旋、CPU 100% 等问题。
- 你的背景:Java 程序员
- 操作系统:Linux Mint XFCE
- Go 版本:go1.22.2 linux/amd64
- 项目目录示例:
/home/liumangmang/GolandProjects/go-atomic-cpu
📌 标题
Go 并发进阶:sync/atomic 原子操作与 CPU 占用分析
✅ 步骤 1:创建练习项目
cd /home/liumangmang/GolandProjects
mkdir go-atomic-cpu && cd go-atomic-cpu
go mod init go-atomic-cpu✅ 步骤 2:用 sync/atomic 做计数器(对标 Java AtomicInteger)
在第 12 天,你用 Mutex 做过并发计数。这次我们用 sync/atomic 实现一个 无锁计数器。
创建 atomic_counter.go:
nano atomic_counter.go粘贴:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var wg sync.WaitGroup
var counter int64 // 注意必须是 int64/uint64 等特定类型
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
atomic.AddInt64(&counter, 1)
}
}()
}
wg.Wait()
fmt.Println("期待的结果:", 1000*1000)
fmt.Println("实际结果:", counter)
}运行:
go run atomic_counter.go你会看到结果稳定为 1_000_000,且没有使用 Mutex。
Java 类比:
AtomicInteger.incrementAndGet()/addAndGet()。- 适用于 非常简单的数值操作 场景:加减、CAS 更新等;
- 若逻辑稍复杂,就更适合 Mutex 或 channel。
✅ 步骤 3:CompareAndSwap 模式(CAS)
CAS 是很多无锁算法的基础:
- “如果现在的值仍然等于旧值,就更新成新值;否则更新失败,再重试”。
创建 atomic_cas.go:
nano atomic_cas.go粘贴:
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var value int64 = 0
success := atomic.CompareAndSwapInt64(&value, 0, 42)
fmt.Println("第一次 CAS 是否成功:", success, "当前值:", value)
success = atomic.CompareAndSwapInt64(&value, 0, 100)
fmt.Println("第二次 CAS 是否成功:", success, "当前值:", value)
}运行:
go run atomic_cas.go输出类似:
第一次 CAS 是否成功: true 当前值: 42
第二次 CAS 是否成功: false 当前值: 42类比 Java:
compareAndSet(expected, update)。
✅ 步骤 4:CPU 100% 的典型错误写法(忙等)
Java 中的坑:
while (!flag) {}忙等,CPU 飙升。
Go 中一样会踩:
创建 cpu_busy_loop.go:
nano cpu_busy_loop.go粘贴:
package main
import (
"fmt"
"sync/atomic"
"time"
)
func main() {
var stop int32 = 0
go func() {
for atomic.LoadInt32(&stop) == 0 {
// 忙等:什么也不干,不让出 CPU
}
fmt.Println("worker 退出")
}()
time.Sleep(2 * time.Second)
fmt.Println("main: 设置 stop=1")
atomic.StoreInt32(&stop, 1)
time.Sleep(1 * time.Second)
}运行时,另外开一个终端,用 top 或 htop 看 CPU 占用,你会发现某个 Go 进程占了一整个核。
这是典型的“忙等”(busy waiting),会让 CPU 始终 100%。
✅ 步骤 5:用 channel 或适当让出 CPU 降低占用
更好的方式:
- 要么用 channel 等待(阻塞时不占用 CPU);
- 要么在循环中适当
time.Sleep或runtime.Gosched()让出时间片。
方案一:用 channel 代替忙等
创建 cpu_channel_wait.go:
nano cpu_channel_wait.go粘贴:
package main
import (
"fmt"
"time"
)
func main() {
stop := make(chan struct{})
go func() {
select {
case <-stop:
fmt.Println("worker 收到停止信号,退出")
}
}()
time.Sleep(2 * time.Second)
fmt.Println("main: 关闭 stop channel")
close(stop)
time.Sleep(1 * time.Second)
}这里 goroutine 会 阻塞在 channel 上,几乎不占 CPU。
方案二:在忙等中适当 Sleep(不推荐但可对比)
for atomic.LoadInt32(&stop) == 0 {
time.Sleep(1 * time.Millisecond) // 或 runtime.Gosched()
}总体建议:优先用 channel / select / context 表达等待,不要手写忙等。
✅ 步骤 6:简单的 CPU 占用分析流程
在生产项目中,你可能会遇到:
- 程序 CPU 突然飙高;
- 某些 goroutine 死循环或高频自旋。
这里给一个最小的实践路径(Linux 环境):
用 top 找到进程:
top观察哪个
go-xxx占用 CPU 很高。使用 runtime/pprof(简单版):
为了不复杂化,这里只演示最基本的写 CPU profile 到文件(需要有一定 Go 测试基础时再实践):
在 main 中引入:
import ( "log" "os" "runtime/pprof" ) func main() { f, err := os.Create("cpu.prof") if err != nil { log.Fatal(err) } pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() // ... 你的业务逻辑 ... }运行程序一段时间后退出,会生成
cpu.prof;使用:
go tool pprof cpu.prof然后在交互界面里用
top,list等命令看哪些函数最耗 CPU。
等你对 Go 更熟悉后,可以进一步学习
net/http/pprof+ 浏览器可视化分析。
✅ 步骤 7:atomic vs Mutex:如何选择?
- 优先考虑 Mutex 或 channel:
- 代码更容易理解;
- 出问题时更容易排查。
- 在极少数性能敏感“数值累加/标记位”场景下用 atomic:
- 如:计数器、状态标志、统计指标;
- 原子操作可以减少锁竞争,但可读性下降。
Java 中也是类似:大多数场景用
synchronized/ReentrantLock足够,只有在热点计数器或高性能队列里才大量用 atomic + CAS。
📚 今日小结与练习
你已经掌握:
sync/atomic的基础用法:Add*、Load*、Store*、CompareAndSwap*。- 忙等循环会导致 CPU 高占用,应尽量用 channel/ctx/select 等结构化方式等待。
- 初步知道如何用
top与pprof分析 CPU 占用高的问题。
练习建议:
- 修改
atomic_counter.go,在计数完成后再开几个 goroutine 做只读统计,使用atomic.LoadInt64读取结果。 - 把第 12 天用 Mutex 的计数器改为 atomic 版本,对比两种写法的复杂度和可读性。
- 尝试在一个“错误示例”中加入 pprof,刻意制造一个死循环,生成
cpu.prof后用go tool pprof看一下top函数列表。
- 修改
