读写锁和互斥锁的性能比较 定义互斥锁和读写锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 type RW interface { Write() Read() }const cost = time.Microsecondtype Lock struct { count int mu sync.Mutex }func (l *Lock) Write() { l.mu.Lock() l.count++ time.Sleep(cost) l.mu.Unlock() }func (l *Lock) Read() { l.mu.Lock() time.Sleep(cost) _ = l.count l.mu.Unlock() }type RWLock struct { count int mu sync.RWMutex }func (l *RWLock) Write() { l.mu.Lock() time.Sleep(cost) l.count++ l.mu.Unlock() }func (l *RWLock) Read() { l.mu.RLock() _ = l.count time.Sleep(cost) l.mu.RUnlock() }
使用benchmark 测试互斥锁和读写锁的性能差异
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 var wg sync.WaitGroupfunc benchmark (b *testing.B, lock RW, read, write int ) { for i := 0 ; i < b.N; i++ { for j := 0 ; j < read*100 ; j++ { wg.Add(1 ) go func () { lock.Read() wg.Done() }() } for k := 0 ; k < write*100 ; k++ { wg.Add(1 ) go func () { lock.Write() wg.Done() }() } wg.Wait() } }func BenchmarkReadMore (b *testing.B) { benchmark(b, &Lock{}, 9 , 1 ) }func BenchmarkReadMoreWithRW (b *testing.B) { benchmark(b, &RWLock{}, 9 , 1 ) }func BenchmarkReadWriteEqual (b *testing.B) { benchmark(b, &RWLock{}, 5 , 5 ) }func BenchmarkWriteMore (b *testing.B) { benchmark(b, &Lock{}, 1 , 9 ) }func BenchmarkWriteMoreWithRW (b *testing.B) { benchmark(b, &RWLock{}, 1 , 9 ) }
测试结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Forrest@LAPTOP-32G4HDVI MINGW64 /d/Go_WorkSpace/面经/mutex (main) $ go test -bench . goos: windows goarch: amd64 pkg: deom cpu: AMD Ryzen 7 5800H with Radeon Graphics BenchmarkReadMore-16 1 13955332000 ns/op BenchmarkReadMoreWithRW-16 1 1462559300 ns/op BenchmarkReadWriteEqual-16 1 7310712800 ns/op BenchmarkWriteMore-16 1 14232708800 ns/op BenchmarkWriteMoreWithRW-16 1 12806891200 ns/op PASS ok deom 50.568s
可以看出 读写锁在读多的情形下更省时间
互斥锁有两种状态:正常状态和饥饿状态。
在正常状态下,所有等待锁的 goroutine 按照FIFO顺序等待。唤醒的 goroutine 不会直接拥有锁,而是会和新请求锁的 goroutine 竞争锁的拥有。新请求锁的 goroutine 具有优势:它正在 CPU 上执行,而且可能有好几个,所以刚刚唤醒的 goroutine 有很大可能在锁竞争中失败。在这种情况下,这个被唤醒的 goroutine 会加入到等待队列的前面。 如果一个等待的 goroutine 超过 1ms 没有获取锁,那么它将会把锁转变为饥饿模式。
在饥饿模式下,锁的所有权将从 unlock 的 goroutine 直接交给交给等待队列中的第一个。新来的 goroutine 将不会尝试去获得锁,即使锁看起来是 unlock 状态, 也不会去尝试自旋操作,而是放在等待队列的尾部。
如果一个等待的 goroutine 获取了锁,并且满足一以下其中的任何一个条件:(1)它是队列中的最后一个;(2)它等待的时候小于1ms。它会将锁的状态转换为正常状态。
正常状态有很好的性能表现,饥饿模式也是非常重要的,因为它能阻止尾部延迟的现象。
sync.Once 可以使用 sync.Once
实现单例模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var instance *Instancevar once sync.Oncetype Instance struct { }func GetInstance () *Instance { once.Do(func () { instance = &Instance{} }) return instance }
sync.Once
的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 type Once struct { done uint32 m Mutex }func (o *Once) Do(f func () ) { if atomic.LoadUint32(&o.done) == 0 { o.doSlow(f) } }func (o *Once) doSlow(f func () ) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1 ) f() } }
done 在热路径中,done 放在第一个字段,能够减少 CPU 指令,也就是说,这样做能够提升性能。
简单解释下这句话:
热路径(hot path)是程序非常频繁执行的一系列指令,sync.Once 绝大部分场景都会访问 o.done
,在热路径上是比较好理解的,如果 hot path 编译后的机器码指令更少,更直接,必然是能够提升性能的。
为什么放在第一个字段就能够减少指令呢?因为结构体第一个字段的地址和结构体的指针是相同的,如果是第一个字段,直接对结构体的指针解引用即可。如果是其他的字段,除了结构体指针外,还需要计算与第一个值的偏移(calculate offset)。在机器码中,偏移量是随指令传递的附加值,CPU 需要做一次偏移值与指针的加法运算,才能获取要访问的值的地址。因为,访问第一个字段的机器代码更紧凑,速度更快。
sync.Pool 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var buf, _ = json.Marshal(&Student{ name: "YST" , Score: 19 , })type Student struct { name string Score int }var StudentPool = sync.Pool{New: func () interface {} { return new (Student) }}func getStu () *Student { return StudentPool.Get().(*Student) }func putStu (stu *Student) { StudentPool.Put(stu) }
定义一个student
结构体和studentPool
的pool池
封装get和put操作
写benchmark 测试 有 pool和无的内存分配的区别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func BenchmarkMarshal (b *testing.B) { for i := 0 ; i < b.N; i++ { stu := &Student{} err := json.Unmarshal(buf, stu) if err != nil { fmt.Println("err:" , err) continue } } }func BenchmarkMarshalWithPool (b *testing.B) { for i := 0 ; i < b.N; i++ { stu := getStu() err := json.Unmarshal(buf, stu) if err != nil { fmt.Println("err:" , err) continue } putStu(stu) } }
1 go test -bench . -benchmem
测试结果:可以看出使用 **pool ** 的每次操作所分配内存会更少
1 2 3 4 5 6 7 8 9 10 Forrest@LAPTOP-32G4HDVI MINGW64 /d/Go_WorkSpace/面经/pool (main) $ go test -bench . -benchmem goos: windows goarch: amd64 pkg: demo cpu: AMD Ryzen 7 5800H with Radeon Graphics BenchmarkMarshal-16 2651370 454.0 ns/op 248 B/op 6 allocs/op BenchmarkMarshalWithPool-16 2634880 442.2 ns/op 224 B/op 5 allocs/op PASS ok demo 4.011s