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

4 回复

你的示例有问题。你看过 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 之所以快,是因为它被翻译成一组特殊的机器指令。而且,锁是依赖于操作系统的。看看这个基准测试的结果:

stackoverflow.com

那个帖子包含了很多有用的信息。还有这个:

stackoverflow.com

将原子基准测试重写为使用 Store/Load 可能会很有趣。话虽如此,根据文档

这些函数需要非常小心才能正确使用。除了特殊的低级应用程序外,同步最好通过通道或 sync 包的功能来完成。通过通信来共享内存;不要通过共享内存来通信。

为了说明这一点,go.dev 上有一个优秀的代码讲解:

go.dev

内容很密集,但对于理解 Go 谚语“通过通信来共享内存;不要通过共享内存来通信”非常有帮助。

In scenarios with high concurrency, what choices should we make?

我倾向于一个强烈的“视情况而定”。这不是一个令人满意的答案,但几乎总是就是答案。你应该编写你的实际应用程序,然后开始对那个应用程序进行基准测试。

从基准测试结果可以看出,当需要执行多个原子操作时,使用互斥锁的性能确实优于多个独立的原子操作。这是因为每个原子操作都需要独立的内存屏障和缓存同步,而互斥锁在一次加锁解锁过程中保护了所有字段的更新。

以下是具体分析:

  1. 原子操作的开销:每个atomic.AddInt64()都包含完整的内存屏障(memory barrier),确保操作顺序性和可见性。当连续执行4个原子操作时,会产生4次内存屏障开销。

  2. 互斥锁的优化:现代互斥锁(如sync.Mutex)在低竞争情况下性能很好,特别是在ARM64架构上。一次加锁解锁只产生一次完整的内存屏障。

  3. 性能对比

    • 2个goroutine时:原子操作25ns vs 互斥锁83ns
    • 4个goroutine时:原子操作51ns vs 互斥锁96ns
    • 8个goroutine及以上:原子操作70-130ns vs 互斥锁75-85ns

当并发goroutine数达到8个时,互斥锁性能开始反超。这是因为原子操作在高并发下缓存行(cache line)竞争加剧。

选择建议

  1. 单个变量更新:优先使用原子操作
// 单个计数器使用原子操作
type Counter struct {
    value int64
}

func (c *Counter) Add(delta int64) {
    atomic.AddInt64(&c.value, delta)
}
  1. 多个相关变量需要原子性更新:使用互斥锁
// 多个相关字段使用互斥锁
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++
}
  1. 读多写少场景:考虑使用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
}
  1. 极端性能要求:考虑使用无锁数据结构或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()
}

关键结论

  • 需要原子性更新多个相关变量时,使用互斥锁
  • 只需要原子性更新单个变量时,使用原子操作
  • 高并发场景下,多个原子操作的开销可能超过单个互斥锁
  • 实际选择应根据具体场景和基准测试结果决定
回到顶部