版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | http://vearne.cc

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

参考资料

  1. Go 中的锁源码实现:Mutex
  2. 深入Golang之sync.mutex

如果我的文章对你有帮助,你可以给我打赏以促使我拿出更多的时间和精力来分享我的经验和思考总结。

微信支付码

发表评论

电子邮件地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据