Go 原子操作:sync/atomic 包的使用
在并发编程中,多个 goroutine 同时读写同一个变量时,如果不加保护,会导致数据竞争(data race),产生不可预测的结果。Go 提供了 sync/atomic 包,用于实现对基本类型(如整数、指针)的无锁原子操作。这些操作由 CPU 指令直接保证,比使用互斥锁(sync.Mutex)更轻量、更高效。
何时使用 atomic?
优先考虑 sync/atomic 的场景:
- 只需要对单个变量进行简单操作(如自增、比较并交换)。
- 性能敏感,希望避免锁带来的开销。
- 不涉及复杂逻辑(如多个变量需同时更新)。
不要使用 atomic 的情况:
- 需要操作结构体字段(除非整个结构体用指针原子替换)。
- 多个变量之间存在一致性要求(此时应使用
sync.Mutex或sync.RWMutex)。
基础整数原子操作
Go 的 atomic 包为有符号和无符号整数提供了统一的操作接口。最常用的是 int64 和 uint64 类型。
1. 加载 当前值
调用 atomic.LoadInt64 安全读取一个 int64 变量:
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var counter int64 = 100
value := atomic.LoadInt64(&counter)
fmt.Println(value) // 输出: 100
}
注意:必须传入变量的地址(
&counter),因为函数内部通过指针直接读取内存。
2. 存储 新值
调用 atomic.StoreInt64 安全写入一个新值:
var counter int64
atomic.StoreInt64(&counter, 42)
fmt.Println(atomic.LoadInt64(&counter)) // 输出: 42
3. 自增/自减
调用 atomic.AddInt64 实现原子加法(支持负数,即减法):
var counter int64
atomic.AddInt64(&counter, 5) // +5
atomic.AddInt64(&counter, -2) // -2
fmt.Println(atomic.LoadInt64(&counter)) // 输出: 3
所有
AddXXX函数都返回操作后的新值,可直接使用。
4. 比较并交换(CAS)
调用 atomic.CompareAndSwapInt64 实现“仅当当前值等于旧值时才更新”:
var counter int64 = 10
success := atomic.CompareAndSwapInt64(&counter, 10, 20)
fmt.Println(success, atomic.LoadInt64(&counter)) // true, 20
// 再次尝试:期望值是 10,但实际是 20,失败
success = atomic.CompareAndSwapInt64(&counter, 10, 30)
fmt.Println(success, atomic.LoadInt64(&counter)) // false, 20
CAS 是实现无锁数据结构(如队列、栈)的核心原语。
指针的原子操作
除了整数,atomic 还支持对指针的原子加载、存储和 CAS。
原子替换结构体指针
假设有一个配置对象,需要在运行时安全地热更新:
package main
import (
"fmt"
"sync/atomic"
"time"
)
type Config struct {
Host string
Port int
}
func main() {
var current atomic.Value // 使用 atomic.Value 更通用(见下文)
// 但若坚持用指针:
var cfgPtr unsafe.Pointer
// 初始配置
initial := &Config{Host: "localhost", Port: 8080}
atomic.StorePointer(&cfgPtr, unsafe.Pointer(initial))
// 读取配置
readCfg := (*Config)(atomic.LoadPointer(&cfgPtr))
fmt.Println(readCfg.Host) // localhost
// 更新配置
newCfg := &Config{Host: "127.0.0.1", Port: 9090}
atomic.StorePointer(&cfgPtr, unsafe.Pointer(newCfg))
// 读取新配置
readCfg = (*Config)(atomic.LoadPointer(&cfgPtr))
fmt.Println(readCfg.Host) // 127.0.0.1
}
⚠️ 使用
unsafe.Pointer需谨慎。Go 官方推荐优先使用atomic.Value(下节介绍)。
更通用的 atomic.Value
atomic.Value 是一个泛型容器(Go 1.18+ 前通过 interface{} 实现),可原子地存储和加载任意类型的值,无需指针转换,且类型安全。
安全地发布和读取配置
package main
import (
"fmt"
"sync/atomic"
)
type DBConfig struct {
DSN string
Pool int
}
func main() {
var config atomic.Value
// 存储初始配置
config.Store(DBConfig{DSN: "root@tcp", Pool: 10})
// 在另一个 goroutine 中读取
go func() {
if cfg, ok := config.Load().(DBConfig); ok {
fmt.Println("DSN:", cfg.DSN)
}
}()
// 更新配置
config.Store(DBConfig{DSN: "user@tcp", Pool: 20})
}
关键规则:
- 第一次
Store决定了后续只能存储相同类型的值。 Load()返回interface{},需类型断言(或 Go 1.18+ 泛型封装)。
整数类型对应表
Go 的 atomic 包为不同整数类型提供了独立函数。以下是完整映射:
| Go 类型 | Load 函数 | Store 函数 | Add 函数 | CAS 函数 |
|---|---|---|---|---|
int32 |
LoadInt32 |
StoreInt32 |
AddInt32 |
CompareAndSwapInt32 |
int64 |
LoadInt64 |
StoreInt64 |
AddInt64 |
CompareAndSwapInt64 |
uint32 |
LoadUint32 |
StoreUint32 |
AddUint32 |
CompareAndSwapUint32 |
uint64 |
LoadUint64 |
StoreUint64 |
AddUint64 |
CompareAndSwapUint64 |
uintptr |
LoadUintptr |
StoreUintptr |
AddUintptr |
CompareAndSwapUintptr |
注意:没有
int或uint的原子操作(因平台相关,32/64 位不一致)。始终显式使用int32/int64。
实战:无锁计数器
下面是一个线程安全的计数器,支持并发自增和获取当前值:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
type Counter struct {
val int64
}
func (c *Counter) Increment() {
atomic.AddInt64(&c.val, 1)
}
func (c *Counter) Value() int64 {
return atomic.LoadInt64(&c.val)
}
func main() {
var wg sync.WaitGroup
counter := &Counter{}
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
counter.Increment()
}
}()
}
wg.Wait()
fmt.Println(counter.Value()) // 必定输出 100000
}
对比使用 sync.Mutex 的版本,此实现无锁,性能更高,尤其在高并发场景。
注意事项与陷阱
-
不要混合原子与非原子访问
对同一个变量,所有读写都必须通过atomic函数。否则仍会引发数据竞争。 -
atomic 不是万能的
若需原子性地更新多个字段(如账户余额和日志),必须用锁。例如:// ❌ 错误:两个原子操作无法保证整体原子性 atomic.AddInt64(&balance, -100) atomic.AddInt64(&logCount, 1) // ✅ 正确:用互斥锁包裹 mu.Lock() balance -= 100 logCount++ mu.Unlock() -
64 位对齐问题(仅 32 位平台)
在 32 位系统上,int64/uint64变量必须 64 位对齐,否则atomic操作 panic。
解决方案:将int64字段放在 struct 的第一个位置,或使用atomic.Value。 -
CAS 循环可能忙等待
实现无锁算法时,CAS 失败需重试,可能导致 CPU 占用高。应结合runtime.Gosched()或退避策略。
性能对比:atomic vs mutex
以下基准测试展示两者差异:
package main
import (
"sync"
"sync/atomic"
"testing"
)
var (
atom int64
mu sync.Mutex
mtx int64
)
func BenchmarkAtomic(b *testing.B) {
for i := 0; i < b.N; i++ {
atomic.AddInt64(&atom, 1)
}
}
func BenchmarkMutex(b *testing.B) {
for i := 0; i < b.N; i++ {
mu.Lock()
mtx++
mu.Unlock()
}
}
运行 go test -bench=. 典型结果:
BenchmarkAtomic-8 100000000 10.2 ns/op
BenchmarkMutex-8 30000000 42.1 ns/op
结论:对于单一变量的简单操作,atomic 比 mutex 快 3-5 倍。
总结使用步骤
- 确定需求:是否只需操作单个整数或指针?
- 选择类型:明确使用
int64/uint64等具体类型(避免int)。 - 统一访问:对该变量的所有读写都通过
atomic函数。 - 复杂场景回退:若涉及多变量或复杂逻辑,改用
sync.Mutex。 - 优先
atomic.Value:对结构体或接口类型,使用atomic.Value避免unsafe。

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