前言
Go 提供内存同步机制,例如通道或互斥锁,这些机制有助于解决不同的问题。在共享内存的情况下,互斥锁保护内存不受数据竞争的影响。然而,尽管存在两个互斥锁,Go 还是通过原子包提供原子内存原语来提高性能。在深入研究解决方案之前,让我们先回顾一下数据竞赛。
当两个或多个 goroutine 并发访问相同的内存位置并且其中至少有一个正在写入时,可能会发生数据竞争。虽然映射有一个本地机制来防止数据竞争,但是一个简单的结构没有任何数据竞争,这使得它容易受到数据竞争的攻击。
一 atomic
atomic 官方描述
Package atomic provides low-level atomic memory primitives useful for implementing synchronization algorithms.
包原子提供了用于实现同步算法的低级原子内存原语。
These functions require great care to be used correctly. Except for special, low-level applications, synchronization is better done with channels or the facilities of the sync package. Share memory by communicating; don't communicate by sharing memory.
正确使用这些功能需要非常小心。除了特殊的底层应用程序,同步最好是通过通道或同步包的设施来完成。通过交流来共享内存,而不是通过共享内存来交流。
1.方法展示
add 类型的 在 addr 的基础上 加或者减 delta
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
CompareAndSwapXXX 类型的 CAS 操作 会先比较传入的地址的值是否是 old,如果是的话就尝试赋新值,如果不是的话就直接返回 false,返回 true 时表示赋值成功
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
LoadIntxxx 从某个地址中取值
func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
StoreIntXXX 给某个地址赋值
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
SwapIntXXX 交换两个值返回旧的值
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
任意类型的取值和赋值
type Value
func (v *Value) Load() (x interface{})
func (v *Value) Store(x interface{})
二 案例
读多写少的状态下Mutex 和 Atomic.value 的对比
func BenchmarkRWMutex(t *testing.B) {
var l sync.RWMutex
var cfg *Config
go func() {
i := 0
for {
i++
l.Lock()
cfg = &Config{
data: []int{i, i + 1, i + 2, i + 3, i + 4, i + 5},
}
l.Unlock()
}
}()
var wg sync.WaitGroup
for n := 0; n < 4; n++ {
wg.Add(1)
go func() {
for n := 0; n < t.N; n++ {
l.RLock()
cfg.T()
l.RUnlock()
}
wg.Done()
}()
}
wg.Wait()
}
func BenchmarkAtomic(t *testing.B) {
var v atomic.Value
v.Store(&Config{})
go func() {
i := 0
for {
i++
cfg := &Config{
data: []int{i,i+1,i+2,i+3,i+4,i+5},
}
v.Store(cfg)
}
}()
var wg sync.WaitGroup
for n := 0; n < 4; n++ {
wg.Add(1)
go func() {
for n := 0; n < t.N; n++ {
cfg := v.Load().(*Config)
cfg.T()
//fmt.Println(cfg.data)
}
wg.Done()
}()
}
wg.Wait()
}
结果如下
goos: windows
goarch: amd64
BenchmarkRWMutex-6 3069406 666 ns/op
BenchmarkAtomic-6 625246248 2.25 ns/op
PASS
ok command-line-arguments 5.323s
同样的数据 Mutex 版本的需要 666 纳秒
而 Atomic.value 的版本需要 2.25纳秒 并且没有 不会出现 data race
Mutex 为什么这么慢呢?
Mutex 相对更重。 因为涉及到更多的 goroutine 之间的上下文切换 pack blocking goroutine,以及唤醒 goroutine。Atomic.value 为什么这么快呢?
使用了Copy-On-Write 写时复制的思想
写操作时候复制全量老数据到一个新的对象中,携带上 本次新写的数据, 之后利用原子替换(atomic.Value),更新调用者的变量。来完成无锁访问共享数据
文献
atomic package - sync/atomic - pkg.go.dev 官方文档
总结
这是一个底层库 在实际业务当中 还是使用channel 为好。如果场景需要请小心使用!!!