Golang 的 协程调度机制 与 GOMAXPROCS 性能调优

做者:林冠宏 / 指尖下的幽灵git

掘金:juejin.im/user/587f0d…github

博客:www.cnblogs.com/linguanh/数组

GitHub : github.com/af913337456…并发

腾讯云专栏: cloud.tencent.com/developer/u…函数


前序

正确地认识 G , M , P 三者的关系,可以对协程的调度机制有更深刻的理解! 本文将会完整介绍完 go 协程的调度机制,包含:性能

  • 调度对象的主要组成
  • 各对象的关系 与 分工
  • gorutine 协程是如何被执行的
  • 内核线程 sysmon 对 gorutine 的管理
  • gorutine 协程中断挂起 与 恢复
  • GOMAXPROCS 如何影响 go 的并发性能

BTW:本人技术书籍《区块链以太坊DApp开发实战》现已出版并可网购了,适合初中级区块链技术相关研发人员阅读。区块链

目录

调度器的三个基本对象:

Golang 简称 Go,Go 的协程(goroutine) 和咱们常见的线程(Thread)同样,拥有其调度器。优化

  • G (Goroutine),表明协程,也就是每次代码中使用 go 关键词时候会建立的一个对象
  • M (Work Thread),工做线程
  • P (Processor),表明一个处理器,又称上下文

G-M-P三者的关系与特色:

  • 每个运行的 M 都必须绑定一个 P,线程M 建立后会去检查并执行G (goroutine)对象
  • 每个 P 保存着一个协程G 的队列
  • 除了每一个 P 自身保存的 G 的队列外,调度器还拥有一个全局的 G 队列
  • M 从队列中提取 G,并执行
  • P 的个数就是GOMAXPROCS(最大256),启动时固定的,通常不修改
  • M 的个数和 P 的个数不必定同样多(会有休眠的M 或 P不绑定M )(最大10000)
  • P 是用一个全局数组(255)来保存的,而且维护着一个全局的 P 空闲链表

局部G队列与全局G队列的关系

  • 全局G任务队列会和各个本地G任务队列按照必定的策略互相交换。没错,就是协程任务交换
  • G任务的执行顺序是,先从本地队列找,本地没有则从全局队列
  • 转移
    • 局部与全局,全局G个数 / P个数
    • 局部与局部,一次性转移一半

Gorutine从入队到执行

  1. 当咱们建立一个G对象,就是 gorutine,它会加入到本地队列或者全局队列
  2. 若是还有空闲的P,则建立一个M 绑定该 P ,注意!这里,P 此前必须还没绑定过M 的,不然不知足空闲的条件。细节点:
    1. 先找到一个空闲的P,若是没有则直接返回
    2. P 个数不会占用超过本身设定的cpu个数
    3. P 在被 M 绑定后,就会初始化本身的 G 队列,此时是一个空队列
    4. 注意这里的一个点
      • 不管在哪一个 M 中建立了一个 G,只要 P 有空闲的,就会引发新 M 的建立
      • 不需考虑当前所在 M 中所绑的 P 的 G 队列是否已满
      • 新建立的 M 所绑的 P 的初始化队列会从其余 G 队列中取任务过来
    5. 这里留下第一个问题:

      若是一个G任务执行时间太长,它就会一直占用 M 线程,因为队列的G任务是顺序执行的,其它G任务就会阻塞,如何避免该状况发生? --①spa

  3. M 会启动一个底层线程循环执行能找到的 G 任务。这里的寻找的 G 从下面几方面找:
    • 当前 M 所绑的 P 队列中找
    • 去别的 P 的队列中找
    • 去全局 G 队列中找
  4. G任务的执行顺序是,先从本地队列找,本地没有则从全局队列找
  5. 程序启动的时候,首先跑的是主线程,而后这个主线程会绑定第一个 P
  6. 入口 main 函数,实际上是做为一个 goroutine 来执行

解答问题-①

协程的切换时间片是10ms,也就是说 goroutine 最多执行10ms就会被 M 切换到下一个 G。这个过程,又被称为 中断,挂起线程

原理:

go程序启动时会首先建立一个特殊的内核线程 sysmon,用来监控和管理,其内部是一个循环:

  1. 记录全部 P 的 G 任务的计数 schedtick,schedtick会在每执行一个G任务后递增

  2. 若是检查到 schedtick 一直没有递增,说明这个 P 一直在执行同一个 G 任务,若是超过10ms,就在这个G任务的栈信息里面加一个 tag 标记

  3. 而后这个 G 任务在执行的时候,若是遇到非内联函数调用,就会检查一次这个标记,而后中断本身,把本身加到队列末尾,执行下一个G

  4. 若是没有遇到非内联函数 调用(有时候正常的小函数会被优化成内联函数)的话,那就会一直执行这个G任务,直到它本身结束;若是是个死循环,而且 GOMAXPROCS=1 的话。那么一直只会只有一个 P 与一个 M,且队列中的其余 G 不会被执行!

例子,下面的这段代码,hello world 不会被输出

func main(){
    runtime.GOMAXPROCS(1)
    go func(){
    	fmt.Println("hello world")
    	// panic("hello world") // 强制观察输出
    }()
    go func(){
    	for {
            // fmt.Println("aaa") // 非内联函数,这行注释打开,将致使 hello world 的输出
    	}
    }()
    select {}
}
复制代码

中断后的恢复

  1. 中断的时候将寄存器里的栈信息,保存到本身的 G 对象里面
  2. 当再次轮到本身执行时,将本身保存的栈信息复制到寄存器里面,这样就接着上次以后运

GOMAXPROCS--性能调优

看完上面的内容,相信你已经知道,GOMAXPROCS 就是 go 中 runtime 包的一个函数。它设置了 P 的最多的个数。这也就直接致使了 M 最多的个数是多少,而 M 的个数就决定了各个 G 队列能同时被多少个 M 线程来进行调取执行!

故,咱们通常将 GOMAXPROCS 的个数设置为 CPU 的核数,且须要注意的是:

  • go 1.5 版本以前的 GOMAXPROCS 默认是 1
  • go 1.5 版本以后的 GOMAXPROCS 默认是 Num of cpu