1. 前言
在 聊聊golang中的锁(1) 中,笔者提到Golang的锁,可能引发的非常高的CPU消耗,本文我们一起来探究一下,CPU时钟都消耗再了哪里。
2. 分析
修改代码, 使用pprof
package main
import (
"fmt"
"sync"
"time"
_ "net/http/pprof"
"net/http"
"log"
)
type LockBox struct{
sync.Mutex
Value int
}
func deal(wpg *sync.WaitGroup, bp *LockBox, count int){
for i:=0;i< count;i++{
bp.Lock()
bp.Value++
bp.Unlock()
}
wpg.Done()
}
func main() {
go func() {
log.Println(http.ListenAndServe(":18081", nil))
}()
timeStart := time.Now()
workerCount := 100
taskCount := 10000000000
var wg sync.WaitGroup
var box LockBox
for i:=0;i<workerCount;i++{
wg.Add(1)
go deal(&wg, &box, taskCount/workerCount)
}
wg.Wait()
fmt.Println("cost", time.Since(timeStart))
}
记录采样运行数据
go tool pprof --seconds 30 http://192.168.1.100:18081/debug/pprof/profile
得到CPU占比TOP 10的函数
flat flat% sum% cum cum%
20.09s 20.95% 20.95% 48.23s 50.29% sync.(*Mutex).Lock
16.04s 16.72% 37.67% 16.04s 16.72% runtime.futexlock
11.75s 12.25% 49.92% 27.69s 28.87% runtime.lock
6.37s 6.64% 56.56% 6.37s 6.64% runtime.procyield
6.17s 6.43% 63.00% 24.86s 25.92% sync.(*Mutex).Unlock
5.02s 5.23% 68.23% 5.02s 5.23% runtime.cansemacquire
4.23s 4.41% 72.64% 77.32s 80.62% main.deal
2.99s 3.12% 75.76% 2.99s 3.12% runtime.osyield
2.31s 2.41% 78.17% 26.37s 27.49% runtime.semacquire1
2.22s 2.31% 80.48% 2.22s 2.31% runtime.casgstatus
它们之间的调用关系
初步的结论
1) Lock()的成本(占比50%)远高于Unlock的成本(占比25.9%)
2) procyield 是自旋操作,自旋操作的消耗也已经达到了6%
TEXT runtime·procyield(SB),NOSPLIT,$0-0
MOVL cycles+0(FP), AX
again:
PAUSE
SUBL $1, AX
JNZ again
RET
PAUSE 相当于是空命令
展开来说
- step1 使用CAS原语(Compare And Swap) 抢一个内存中的变量
- step3 runtime.semacquire1 就是在争夺信号量,其中会用到,sema自己实现的一个简单的锁(此步操作有系统调用futex)
3. 感想
在阅读代码的过程中,我有强烈的感觉,Golang的设计者有强烈的意愿在避免触发某些系统调用,以至于不惜进行许多额外操作比如竞争内存锁(step1), 自旋等等
** 注意** Golang在锁在竞争激烈的情况,有很高的CPU开销,需要确保锁的粒度足够小。在工作中,我们采用分段锁的方式来避免过多的竞争。