锁CAS原子操作
如要对一个变量进行计数统计,两种实现方式分别为
package main
import (
"fmt"
"sync"
)
// 锁实现方式
func main() {
var count int64
var wg sync.WaitGroup
var mu sync.Mutex
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock()
count = count + 1
mu.Unlock()
}(&wg)
}
wg.Wait()
// count = 10000
fmt.Println("count = ", count)
}
与
package main
import (
"fmt"
"sync"
"sync/atomic"
)
// atomic CAS 原子操作
func main() {
var count int64
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(wg *sync.WaitGroup) {
defer wg.Done()
// 失败一直重试
for {
old := atomic.LoadInt64(&count)
if atomic.CompareAndSwapInt64(&count, old, old+1) {
break
}
}
}(&wg)
}
wg.Wait()
// count = 10000
fmt.Println("count = ", count)
}
可以看到两种用法的执行结果是一样的,我们再看一下两者的性能区别。
性能差别package main_test
import (
"sync"
"sync/atomic"
"testing"
)
func lockTest() {
var count int64
var wg sync.WaitGroup
var mu sync.Mutex
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock()
count = count + 1
mu.Unlock()
}(&wg)
}
wg.Wait()
}
func atomicTest() {
var count int64
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(wg *sync.WaitGroup) {
defer wg.Done()
// 通过
for {
old := atomic.LoadInt64(&count)
if atomic.CompareAndSwapInt64(&count, old, old+1) {
break
}
}
}(&wg)
}
wg.Wait()
}
func BenchmarkLock(b *testing.B) {
for i := 0; i < b.N; i++ {
lockTest()
}
}
func BenchmarkAtomic(b *testing.B) {
for i := 0; i < b.N; i++ {
atomicTest()
}
}
下面分别用一个、两个、四个CPU来测试他们两者的性能差别
一个CPU
➜ gotest go test -bench=".*" -v -benchmem -cpu=1 goos: darwin goarch: amd64 pkg: gotest cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz BenchmarkLock BenchmarkLock 194 6312940 ns/op 32 B/op 3 allocs/op BenchmarkAtomic BenchmarkAtomic 189 6342975 ns/op 24 B/op 2 allocs/op PASS ok gotest 3.296s
在只用一个CPU的情况下,看着差别不是太大,每次 op 的内存使用 atomic 比 lock 少了1/3。
两个CPU
➜ gotest go test -bench=".*" -v -benchmem -cpu=2 goos: darwin goarch: amd64 pkg: gotest cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz BenchmarkLock BenchmarkLock-2 350 3178037 ns/op 2071 B/op 7 allocs/op BenchmarkAtomic BenchmarkAtomic-2 364 3035188 ns/op 439 B/op 3 allocs/op PASS ok gotest 3.370s
可以看到相比一个CPU来说,每个 op 的时间基于快了一倍,分配的内存也大大减小了。
四个CPU
➜ gotest go test -bench=".*" -v -benchmem -cpu=4 goos: darwin goarch: amd64 pkg: gotest cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz BenchmarkLock BenchmarkLock-4 349 3462298 ns/op 3613 B/op 17 allocs/op BenchmarkAtomic BenchmarkAtomic-4 330 3382266 ns/op 24 B/op 2 allocs/op PASS ok gotest 4.221s
opopallocs
总结
atomiclock
atomiclock
实现原理
硬件底层
但原子也有一定的弊端,在被操作值频繁变更的情况下,很可能失败,需要一直重试直到成功为止,这种重试行为也可以理解为自旋spinning,长时间处于spinning将浪费CPU,参考https://www.bilibili.com/video/BV1Sf4y1s7Np/。
原子操作
从硬件的层面实现原子操作,有两种方式:
LOCK#
X86CMPXCHG
CAS 在Golang中是以共享内存的方式来实现的一种同步机制,它是一个原子操作,一般格式如下
fun addValue(delta int32){
for{
oldValue := atomic.LoadInt32(&addr)
if atomic.CompareAndSwapInt32(&addr, oldValue, oldValue+delta){
break;
}
}
}
&addratomic.CompareAndSwapInt32
atomic.CompareAndSwapIntxx()
atomic.CompareAndSwapInt32atomic.CompareAndSwapUint32runtime∕internal∕atomic·Cas
// bool Cas(int32 *val, int32 old, int32 new)
// Atomically:
// if(*val == old){
// *val = new;
// return 1;
// } else
// return 0;
TEXT runtime∕internal∕atomic·Cas(SB),NOSPLIT,$0-17
MOVQ ptr+0(FP), BX
MOVL old+8(FP), AX
MOVL new+12(FP), CX
LOCK
CMPXCHGL CX, 0(BX)
SETEQ ret+16(FP)
RET
atomic.CompareAndSwapInt64atomic.CompareAndSwapUint64runtime∕internal∕atomic·Cas64
// bool runtime∕internal∕atomic·Cas64(uint64 *val, uint64 old, uint64 new)
// Atomically:
// if(*val == *old){
// *val = new;
// return 1;
// } else {
// return 0;
// }
TEXT runtime∕internal∕atomic·Cas64(SB), NOSPLIT, $0-25
MOVQ ptr+0(FP), BX
MOVQ old+8(FP), AX
MOVQ new+16(FP), CX
LOCK
CMPXCHGQ CX, 0(BX)
SETEQ ret+24(FP)
RET
LOCKCMPXCHGQ
LOCK#LOCK#
CMPXCHG
锁
上面我们提到过锁的实现是由操作系统来实现的,所以它的锁粒度要保证最小越好。对锁的理解很简单,这里就不再详细介绍了。
参考资料