前言

Goroutine调度是一个很复杂的机制, 尽管Goy源码中提供了大量的注释,但对起原理没有一个好的理解情况下去读源码收获不是很大。

1. 线程池的缺陷

我们知道,在高并发应用中频繁创建线程会造成不必要的开销,所以有了线程池。线程池中预先保存一定数量的线程,而新任务将不再以创建线程的方式去执行,而是将任务发布到任务队列,线程池中的线程不断地从任务队列中取出任务并执行,可以有效的减少线程创建和销毁所带来的开销

下图展示一个典型的线程池:

为了方便下面的叙述,我们把任务队列中的每一个任务称作G,而G往往代表一个函数。

线程池中的worker线程不断地从任务队列中取出任务并执行。而worker线程的调度则交给操作系统进行调度。

如果worker线程执行的G任务中发生系统调用,则操作系统会将该线程置为阻塞状态,也意味着该线程在怠工,也意味着消费任务队列的worker线程变少了,也就是说线程池消费任务队列的能力变弱了。

如果任务队列中的大部分任务都会进行系统调用,则会让这种状态恶化,大部分worker线程进入阻塞状态,从而任务队列中的任务产生堆积。

解决这个问题的一个思路就是重新审视线程池中线程的数量,增加线程池中线程数量可以一定程度上提高消费能力,但随着线程数量增多,由于过多线程争抢CPU,消费能力会有上限,甚至出现消费能力下降。 如下图所示:

2.Goroutine 调度器

线程书过多,意味着操作系统会不断地切换线程, 频繁的上下文切换就成了性能瓶颈。

Go提供一种机制,可以在线程中自己实现调度,上下文切换更轻量,从而达到了线程数少,而并发数并不少的效果。而线程中调度的就是Goroutine.

早期Go版本,比如1.9.2版本的源码注释中有关于调度器的解释。

Goroutine 调度器的工作就是把“ready-to-run”的goroutine分发到线程中。

Goroutine主要概念如下:

  • G(Goroutine): 即Go协程,每个go关键字都会创建一个协程。
  • M (Machine): 工作线程,在Go中称为Machine
  • P (Processor): 处理器 【go定义的一个概念,不是指CPU】,包含运行Go代码的主要资源,也有调度goroutine的能力

M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队列,P可以调度G交由M执行,关系图如下

图中M是交给操作系统调度的线程,M持有一个P,P将G调度进M中执行。P同时还维护着一个包含G的队列(图中灰色部分),可以按照一定的策略将G调度到M中执行。

P的个数在程序启动时决定,默认情况下等同于CPU的核数,由于M必须持有一个P才可以运行Go代码,所以同时运行的M个数,也即线程数一般等同于CPU的个数,以达到尽可能的使用CPU而又不至于产生过多的线程切换开销。

程序中可以使用runtime.GOMAXPROCS()设置P的个数,在某些IO密集型的场景下可以在一定程度上提高性能。这个后面再详细介绍。

3.调度策略

3.1 队列轮转

上图中可见每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度。

除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死。

3.2 系统调用

上面说到P的个数默认等于CPU核数,每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。类似线程池,Go也提供一个M的池子,需要时从池子中获取,用完放回池子,不够用时就再创建一个。

当M运行的某个G产生系统调用时,如下图所示:

如图所示,当G0即将进入系统调用时,M0将释放P,进而某个空闲的M1获取P,继续执行P队列中剩下的G。而M0由于陷入系统调用而进被阻塞,M1接替M0的工作,只要P不空闲,就可以保证充分利用CPU。

M1的来源有可能是M的缓存池,也可能是新建的。当G0系统调用结束后,根据M0是否能获取到P,将会将G0做不同的处理:

  1. 如果有空闲的P,则获取一个P,继续执行G0。
  2. 如果没有空闲的P,则将G0放入全局队列,等待被其他的P调度。然后M0将进入缓存池睡眠。

3.3 工作量窃取

多个P中维护的G队列有可能是不均衡的,比如下图:

竖线左侧中右边的P已经将G全部执行完,然后去查询全局队列,全局队列中也没有G,而另一个M中除了正在运行的G外,队列中还有3个G待运行。此时,空闲的P会将其他P中的G偷取一部分过来,一般每次偷取一半。偷取完如右图所示。

4.GOMAXPROCS设置对性能的影响

一般来讲,程序运行时就将GOMAXPROCS大小设置为CPU核数,可让Go程序充分利用CPU。

在某些IO密集型的应用里,这个值可能并不意味着性能最好。

理论上当某个Goroutine进入系统调用时,会有一个新的M被启用或创建,继续占满CPU。

但由于Go调度器检测到M被阻塞是有一定延迟的,也即旧的M被阻塞和新的M得到运行之间是有一定间隔的, 所以在IO密集型应用中不妨把GOMAXPROCS设置的大一些,或许会有好的效果。

参考

【协程调度原理】

go协程调度的更多相关文章

  1. Golang 协程调度

    一.线程模型 N:1模型,N个用户空间线程在1个内核空间线程上运行.优势是上下文切换非常快但是无法利用多核系统的优点. 1:1模型,1个内核空间线程运行一个用户空间线程.这种充分利用了多核系统的优势但 ...

  2. skynet源码阅读<5>--协程调度模型

    注:为方便理解,本文贴出的代码部分经过了缩减或展开,与实际skynet代码可能会有所出入.    作为一个skynet actor,在启动脚本被加载的过程中,总是要调用skynet.start和sky ...

  3. 图解Go协程调度原理,小白都能理解

    阅读本文仅需五分钟,golang协程调度原理,小白也能看懂,超实用. 什么是协程 对于进程.线程,都是有内核进行调度,有CPU时间片的概念,进行抢占式调度.协程,又称微线程,纤程.英文名Corouti ...

  4. Openresty Lua协程调度机制

    写在前面 OpenResty(后面简称:OR)是一个基于Nginx和Lua的高性能Web平台,它内部集成大量的Lua API以及第三方模块,可以利用它快速搭建支持高并发.极具动态性和扩展性的Web应用 ...

  5. GO GMP协程调度实现原理 5w字长文史上最全

    1 Runtime简介 Go语言是互联网时代的C,因为其语法简洁易学,对高并发拥有语言级别的亲和性.而且不同于虚拟机的方案.Go通过在编译时嵌入平台相关的系统指令可直接编译为对应平台的机器码,同时嵌入 ...

  6. Kotlin协程解析系列(上):协程调度与挂起

    vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...

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

    作者:林冠宏 / 指尖下的幽灵 掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8 博客:http://www.cnblogs.com/linguan ...

  8. 图解协程调度模型-GMP模型

    现在无论是客户端.服务端或web开发都会涉及到多线程的概念.那么大家也知道,线程是操作系统能够进行运算调度的最小单位,同一个进程中的多个线程都共享这个进程的全部系统资源. 线程 三个基本概念 内核线程 ...

  9. golang中goroutine协程调度器设计策略

    goroutine与线程 /* goroutine与线程1. 可增长的栈os线程一般都有固定的栈内存,通常为2MB,一个goroutine的在其声明周期开始时只有很小的栈(2KB),goroutine ...

随机推荐

  1. 微信小程序云开发-数据库-查询满足条件的数据

    一.查询价格大于10的商品 1.wxml文件 2.js文件 where条件语句:.where({price:db.command.gt(10)}) 3.查询结果 二.查询价格大于等于10的商品 js文 ...

  2. innodb引擎相关参数

    1.innodb_flush_log_at_trx_commit (双一标准之一) 作用:主要控制了innodb将log buffer中的数据写入日志文件并flush磁盘的时间点,取值分别为0.1.2 ...

  3. Docker隔离技术

    前言 Docker系列文章: 此篇是Docker系列的第九篇,之前的文章里面或多或少的提到Docker的隔离技术,但是没有很清楚的去聊这个技术,但是经过这么多文章大家一定对Docker使用和概念有了一 ...

  4. 论文笔记:(CVPR2019)PointWeb: Enhancing Local Neighborhood Features for Point Cloud Processing

    目录 摘要 一.引言 二.相关工作 3D数据表示 点云深度学习 三.我们的方法 3.1 自适应特征调整(AFA)模块 3.1.1 影响函数fimp 3.1.2 关系函数frel 3.1.3 逐元素影响 ...

  5. 在vmware虚拟机下的Window2003服务器下安装IIS服务详细教程——超级详细(解决关于:800a0bb9的解决办法)

    总的来说,就是9步: 1.控制面板添加或者删除程序2.删除想要删的3.打开IIS配置4.开始共享5.导入源码6.配置权限7.网站属性.文档.应用程序配置8.web服务扩展9.访问网站 在安装好虚拟机的 ...

  6. MapReduce框架原理-MapTask和ReduceTask工作机制

    MapTask工作机制 并行度决定机制 1)问题引出 maptask的并行度决定map阶段的任务处理并发度,进而影响到整个job的处理速度.那么,mapTask并行任务是否越多越好呢? 2)MapTa ...

  7. 5.1 剑指 Offer 03. 数组中重复的数字

    类型题:剑指 Offer 03. 数组中重复的数字 找出数组中重复的数字.在一个长度为 n 的数组 nums 里的所有数字都在 0-n-1 的范围内.数组中某些数字是重复的,但不知道有几个数字重复了, ...

  8. 【Lua篇】静态代码扫描分析(四)规则检查

    一.前言 通过前面三篇文章已经初步实现了将Lua源代码文件读取解析成语法树,现在就可以通过得到的语法树进行指定规则的代码扫描检查.下图简单列举了一下单个Lua文件内部的语法关系情况(注意并非真正的类图 ...

  9. Anaconda创建虚拟环境并使用Jupyter-notebook打开虚拟环境

    由于需要使用Tensorflow的1.xx的低版本,但是cmd下载一直报错没有解决,就想着用Anaconda,但是真实环境我已经装上Tensorflow2.0了不想换了,所以就想着再建一个python ...

  10. 小知识get:利用单臂路由实现不同vlan间路由

    一.单臂路由概述 1.1.单臂路由实现不同vlan间通信 链路类型 交换机连接主机的端口为access链路 交换机连接路由器的端口为Trunk链路 子接口 路由器的物理接口可以被划分成多个逻辑接口 每 ...