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

它们之间的调用关系
image_1cpdst0eusksac1enm198uvca9.png-304kB

初步的结论

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开销,需要确保锁的粒度足够小。在工作中,我们采用分段锁的方式来避免过多的竞争。

参考资料


请我喝瓶饮料

微信支付码