Golang中多原子操作与锁的性能对比
Golang中多原子操作与锁的性能对比 以下是一个基准测试示例:
package main
import (
"sync"
"sync/atomic"
)
type AtomicCounter struct {
A int64
B int64
C int64
D int64
}
type MutexCounter struct {
A int64
B int64
C int64
D int64
mu sync.Mutex
}
// 原子递增
func IncrementAtomic(c *AtomicCounter, delta int64) {
atomic.AddInt64(&c.A, delta)
atomic.AddInt64(&c.B, delta)
atomic.AddInt64(&c.C, delta)
atomic.AddInt64(&c.D, delta)
}
// 互斥锁递增
func IncrementMutex(c *MutexCounter, delta int64) {
c.mu.Lock()
defer c.mu.Unlock()
c.A += delta
c.B += delta
c.C += delta
c.D += delta
}
// 原子递增基准测试
func BenchmarkIncrementAtomic(b *testing.B) {
counter := &AtomicCounter{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
IncrementAtomic(counter, 1)
}
})
}
// 互斥锁递增基准测试
func BenchmarkIncrementMutex(b *testing.B) {
counter := &MutexCounter{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
IncrementMutex(counter, 1)
}
})
}
结果显示锁的性能更优:
goos: darwin
goarch: arm64
pkg: t
cpu: Apple M2 Max
BenchmarkIncrementAtomic
BenchmarkIncrementAtomic-2 41731690 25.47 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-2 50620629 25.21 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-2 45867760 25.26 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-4 23364030 51.48 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-4 23443662 51.09 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-4 24704637 50.02 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-8 14516994 77.24 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-8 15323640 70.95 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-8 16269632 74.57 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-12 9426187 127.9 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-12 9647442 118.3 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-12 9670734 116.5 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-16 9547399 127.9 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-16 9255408 129.1 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-16 9288032 123.9 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-1024 10477359 127.6 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-1024 9347586 123.1 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-1024 9970914 104.3 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-2048 12710971 120.5 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-2048 17835810 111.5 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementAtomic-2048 12470020 117.6 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex
BenchmarkIncrementMutex-2 28628401 82.97 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-2 29020346 82.75 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-2 29471953 85.90 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-4 12342710 96.54 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-4 12148924 95.72 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-4 12092522 96.49 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-8 14736597 79.07 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-8 14910420 77.23 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-8 15160263 77.65 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-12 15025918 78.02 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-12 15581076 76.57 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-12 15080223 77.05 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-16 15165836 78.86 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-16 15863791 77.28 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-16 15384672 75.76 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-1024 13133365 84.49 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-1024 13068876 83.09 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-1024 13828316 83.57 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-2048 12515894 82.34 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-2048 12778197 83.96 ns/op 0 B/op 0 allocs/op
BenchmarkIncrementMutex-2048 12766641 82.67 ns/op 0 B/op 0 allocs/op
PASS
4次(甚至2次)原子操作的性能已经不如单个锁。在高并发场景下,我们应该如何选择?
更多关于Golang中多原子操作与锁的性能对比的实战教程也可以访问 https://www.itying.com/category-94-b0.html
你的示例有问题。你看过 sync.Mutex 的内部实现吗?如果你看过,原因就很清楚了。
更多关于Golang中多原子操作与锁的性能对比的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
你好 @Dean_Davidson,感谢你的回答。 一个常见的现实场景是收集统计数据,例如:
type StatsSummary struct {
RPCCount int64
LatencySum int64
RequestSizeSum int64
ResponseSizeSum int64
}
我喜欢multithreading - Which is more efficient, basic mutex lock or atomic integer? - Stack Overflow中的回答。它清晰地说明了上下文切换和忙等待之间的开销。
但根据我的基准测试,结果出乎我的意料,因为这里并没有很多原子操作(即使减少到2个也比互斥锁低),并且在增加并发性之后,atomic.addInt64 的表现不如互斥锁方法稳定。
有几处让我印象深刻。这种使用 sync/atomic 的方式很奇怪。你究竟想解决什么实际问题?
另外,你的互斥锁只执行一次 Lock() 操作,而你在每个循环中却执行了 4 次 atomic.AddInt64 操作。一般来说,sync/atomic 之所以快,是因为它被翻译成一组特殊的机器指令。而且,锁是依赖于操作系统的。看看这个基准测试的结果:
那个帖子包含了很多有用的信息。还有这个:
将原子基准测试重写为使用 Store/Load 可能会很有趣。话虽如此,根据文档:
这些函数需要非常小心才能正确使用。除了特殊的低级应用程序外,同步最好通过通道或 sync 包的功能来完成。通过通信来共享内存;不要通过共享内存来通信。
为了说明这一点,go.dev 上有一个优秀的代码讲解:
内容很密集,但对于理解 Go 谚语“通过通信来共享内存;不要通过共享内存来通信”非常有帮助。
In scenarios with high concurrency, what choices should we make?
我倾向于一个强烈的“视情况而定”。这不是一个令人满意的答案,但几乎总是就是答案。你应该编写你的实际应用程序,然后开始对那个应用程序进行基准测试。
从基准测试结果可以看出,当需要执行多个原子操作时,使用互斥锁的性能确实优于多个独立的原子操作。这是因为每个原子操作都需要独立的内存屏障和缓存同步,而互斥锁在一次加锁解锁过程中保护了所有字段的更新。
以下是具体分析:
-
原子操作的开销:每个
atomic.AddInt64()都包含完整的内存屏障(memory barrier),确保操作顺序性和可见性。当连续执行4个原子操作时,会产生4次内存屏障开销。 -
互斥锁的优化:现代互斥锁(如sync.Mutex)在低竞争情况下性能很好,特别是在ARM64架构上。一次加锁解锁只产生一次完整的内存屏障。
-
性能对比:
- 2个goroutine时:原子操作25ns vs 互斥锁83ns
- 4个goroutine时:原子操作51ns vs 互斥锁96ns
- 8个goroutine及以上:原子操作70-130ns vs 互斥锁75-85ns
当并发goroutine数达到8个时,互斥锁性能开始反超。这是因为原子操作在高并发下缓存行(cache line)竞争加剧。
选择建议:
- 单个变量更新:优先使用原子操作
// 单个计数器使用原子操作
type Counter struct {
value int64
}
func (c *Counter) Add(delta int64) {
atomic.AddInt64(&c.value, delta)
}
- 多个相关变量需要原子性更新:使用互斥锁
// 多个相关字段使用互斥锁
type Account struct {
balance int64
version int64
mu sync.RWMutex
}
func (a *Account) Transfer(amount int64) {
a.mu.Lock()
defer a.mu.Unlock()
a.balance += amount
a.version++
}
- 读多写少场景:考虑使用sync.RWMutex
type Config struct {
settings map[string]string
mu sync.RWMutex
}
func (c *Config) Get(key string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.settings[key]
}
func (c *Config) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.settings[key] = value
}
- 极端性能要求:考虑使用无锁数据结构或sharding
// 使用sharding减少竞争
type ShardedCounter struct {
shards [64]struct {
value int64
pad [56]byte // 填充避免false sharing
}
}
func (c *ShardedCounter) Add(delta int64) {
shard := runtime_procPin() % 64
atomic.AddInt64(&c.shards[shard].value, delta)
runtime_procUnpin()
}
关键结论:
- 需要原子性更新多个相关变量时,使用互斥锁
- 只需要原子性更新单个变量时,使用原子操作
- 高并发场景下,多个原子操作的开销可能超过单个互斥锁
- 实际选择应根据具体场景和基准测试结果决定

