导语 | Golang核心开发人员、goroutine调度的设计者Dmitry Vyukov,在2019年的一个talk里深入浅出地阐述了goroutine调度的设计思想以及一些优化的细节。本文是笔者结合自身经验和认知的一点观后感,采用从零开始层层递进的方法,总结剖析了其背后的软件设计思想,希望对读者更好地理解goroutine调度GMP模型会有所帮助。

前言

视频地址:

https://2019.hydraconf.com/2019/talks/7336ginp0kke7n4yxxjvld/

这个视频我以前看过,近几天刷到便又看了一遍,真是有听君一席话受益匪浅之感。毫不夸张地说,本视频在笔者看过的所有资料中,对于GMP为什么要有Processor这点,讲得最为清楚。视频中对goroutine调度模型的讲解,真可谓深入浅出!下面笔者将自己的一些观感整理分享给大家,还没看过视频的同学,建议先看完本文再去看,收获会更大。

为了表达方便,本文会沿用golang里面的GMP缩写:

  • G —— goroutine

  • M —— 机器线程

  • P —— 对处理器的抽象

一、设计并发编程模型

goroutine调度的设计目标,其实就是设计一种高效的并发编程模型:

  • 从开发的角度只需要一个关键词(go)就能创建一个执行会话,很方便使用,即开发效率是高效的。

  • 从运行态的角度,上述创建的会话也能高效的被调度执行,即运行效率也是高效的。

我们可以近似将goroutine看待为协程(一些代码逻辑+一个栈上下文),如果读者用C/C++造过协程框架的轮子,会很容易理解这点。

:除了高效之外,还有其他几个目标,如无大小限制的goroutine栈,公平的调度策略等。

二、从零开始:从多线程说起

想要实现并发的执行流,最直截了当的,自然就是多线程。由此便得出初始思路:每个goroutine对应一个线程

从并发的功能角度来讲,该方案固然可以实现并发,但性能方面却很不堪,尤其是在并发很重的时候,成千上万个线程的资源占用、创建销毁、调度带来的开销会很巨大。

三、更进一步:线程池的方案

既然线程太多不好,那我们可以很轻易地做出一点改善,控制一下线程数量,如此便得到更进一步的方案:线程池,限定只启动N个线程。

由于该方案下,可能是M个goroutine,N个线程,因而显然需要考虑一个问题:对于一个goroutine,它到底该由哪个线程去执行?我们可以简单地采用一个全局的Global Run Queue,然后让所有线程主动去获取goroutine来执行,示意如下:

这样做在线程少的时候,如果调度行为不是很频繁,可能问题不大。但当线程较多时,就会有scalable的问题,mutex的互斥竞争会非常激烈(考虑到基于时间片的抢占行为,实际上调度必然是很频繁的)。

四、初具雏形:线程分治

在多线程编程领域中,互斥处理可以称得上是“名声在外”,需极其小心地去应对。最常见的解决方案,并不是如何精妙地去lock free,而是直接通过 “数据分治”和“逻辑分治”来避免做复杂的加锁互斥,将各个线程按横向(载荷分组)或纵向(逻辑划分)进行切分来处理工作。

通过数据分治的思想,我们就可以得到改进的方案:每个线程分别处理一批G,进行线程分治。将所有G分开放到各线程自己的存储中,即所谓的Local Run Queue中。示意如下:

:Global Run Queue也还继续存在的,有关它存在的细节非本文重点,这里不做展开。

至此,调度模型已具雏形。

让我们继续分析确认一下,该模型是否真的解决了scalable的问题。上述模型下,为了充分利用CPU,每个线程要按一定的策略去Steal其他线程Local Run Queue里面的G来执行,以免线程之间存在load balance问题(有些太闲,有些又太忙)

因此在线程很多的时候,存在大量的无意义加锁Steal操作,因为其他线程的Local Run Queue可能也常常都是空的。还有另一个问题,由于现在的一些内存资源是绑定在线程上面的,会导致线程数量和资源占用规模紧耦合。当线程数量多的时候,资源消耗也会比较大。

注:在N核的机器环境下,假如我们设定线程池大小为N,由于系统调用的存在(关于系统调用的处理见后文),实际的线程数量会超过N。

五、趋于完善:将资源和线程解耦

既然每个线程一份资源也不合适,那么我们可以仿照线程池的思路,单独做一个资源池,做计算存储分离:把Local Run Queue及相关存储资源都挪出去,并依然限定全局一共N份,即可实现资源规模与系统中的真实线程数量的解耦。线程每次从对应的数据结构(Processor)中获取goroutine去执行,Local Run Queue及其他一些相关存储资源都挂在Processor下。这样加一层Processor的抽象之后,便得到众所周知的GMP模型:

现在的调度模型已趋于完善,不过前面我们主要侧重讲的是如何高效,还未讨论到调度的另一个关键问题:公平性与抢占,接下来我们看看如何实现抢占。

六、还要公平:调度抢占

参考操作系统CPU的调度策略,通常各进程会分时间片,时间片用完了就轮到其他进程。在golang里也可以如此,不能让一些goroutine长期霸占着运行资源不退出,必须实现基于时间片的“抢占”。

那怎么抢占呢,需要监测goroutine执行时间片是否用完了。如果要检查系统中的各种状态变化、事件发生情况,通常会有中断与轮询两种思路,中断是由一个中控方来做检查与控制,而轮询则是各个参与方按一定的策略主动check询问。因此对于goroutine抢占而言,有以下两种解决方案:

  • Signals,通过信号来中断原来的线程执行。

  • Cooperative checks,通过线程间歇性轮询自己check运行的时间片情况来主动暂停。

二者的优劣对比如下:

因为golang其实是有runtime的,而且代码编译生成也都是golang编译器控制的,综合优劣分析,选择后者会比较合理。

对于Cooperative checks的方案,从代码编译生成的角度看,很容易做check指令的埋点。且因为golang本来就要做动态增长栈,在函数入口处会插入检查是否该扩栈的指令,正好利用这一点来做相关的检查实现(这里有一些优化细节,可以使得基于时间片的抢占开销也较小)

插入check指令的做法,会导致该方案存在一个理论缺陷:若有一个死循环,里面的所有代码都不包含check指令,那依然会无法抢占,不过现实中基本不存在这种情况,总会做函数调用、访问channel等类似操作,因此不足为虑。

除此以外还有一个系统调用的问题,当线程一旦进入系统调用后,也会脱离runtime的控制。试想万一系统调用阻塞了呢,基于Cooperative checks的方案,此时又无法进行抢占,是不是整个线程也就罢工了。所以为了维持整个调度体系的高效运转,必然要在进入系统调用之前要做点什么以防患未然。Dmitry这里采用的办法也很直接,对于即将进入系统调用的线程,不做抢占,而是由它主动让出执行权。线程A在系统调用之前handoff让出Processor的执行权,唤醒一个idle线程B来做交接。当线程A从系统调用返回时,不会继续执行,而是将G放到run queue,然后进入idle状态等待唤醒,这样一来便能确保活跃线程数依然与Processor数量相同。

七、设计思想的小结

这里recap一下,把前文涉及到的一些软件设计思想罗列如下:

  • 线程池,通过多线程提供更大的并发处理能力,同时又避免线程过多带来的过大开销。

  • 资源池,对有一定规模约束的资源进行池化管理,如内存池、机器池、协程池等,前面的线程池也可以算作此类。

  • 计算存储分离,分别从逻辑、数据结构两个角度进行设计,规划二者的耦合关系。

加一层,这个是万能大法,不赘述。

  • 中断与轮询,用于监测系统中的各种状态变化、事件变化,通常来讲中断会比轮询更高效。

八、其他内容

本文的重点在GMP模型,因此还有一些其他的内容,文中并未详细展开:

  • Local Run Queue里面的G所创建的G会放到同样的Local Run Queue(如果满了还是会放GRQ),而且会限制被偷走,这样可以加强Locality,同时为了保证公平也做了时间片继承,以免不停创建G会长期霸占运行资源。

  • 被抢占的G会放到全局的G队列(Global Run Queue),GRQ会每61次tick检查一次,Dmitry针对这个61解释了一番,但笔者认为还是有点拍脑袋的感觉。

  • G的栈采用的是Growable stack方案,在函数入口会有栈检查的指令,如需扩容栈,会拷贝到新申请的更大的栈。

  • Go runtime还会用Background thread来运行一些相对特别的G(如 Network Poller、Timer)。

以上这些内容,大家可以去视频学习。

:本文基于2019的talk,不知最新版本的调度机制是否有进一步的调整,不过无论调整与否,这并不妨碍我们对GMP设计思想的学习。

九、进一步的改进

有同学在与笔者讨论时提了一个问题:还可以怎么继续优化,这真的是一个非常好的问题,这里将该问题的回答也放入文章。

不单纯针对GMP,话题稍微放大一点,下面简单聊聊goroutine调度机制的一些优化可能。

Dmitry自己在视频最后说的future work方向:

  • 在很多cpu core的情况下,活跃线程数比较多,work steal的开销依旧有些浪费。

  • 死循环不含cooperative check指令的这种edge情况的还没解决。

  • 对于网络和timer的goroutine处理是使用全局方式的,不好scale。

以下纯属个人探讨:

  • 首先整体上现在的模型已经比较完善,如何进一步优化要看实践场景遇到的问题,以及profile数据情况,只有问题和数据明确了,才清楚进一步工作的宏观重点(工作中也是,做性能优化需要有宏观视角)。

  • 因为goroutine调度是属于协程类的调度,这里或许可以借鉴原来各种协程框架的思路做一些对比考虑。

  • 由于笔者并没细看过代码,不大清楚work steal的overhead构成,或许可以设计其他的rebalance方式,例如换个视角,不是去steal,而是由runtime统一rebalance再收集派发。

目前就先想到这些,欢迎讨论。

十、欢乐游戏的协程框架

基于上面那个问题的回答,这里也补充介绍一下欢乐游戏协程框架(基于C++)中采用的处理机制,因为是纯业务自用,所以从设计要求上就低很多,不少点直接都可以不去考虑(这也说明了,有些时候再好的既有流行方案,从性能上讲可能也比不过自家的破轮子,当然自家的轮子泛化不足,肯定普适性就会差很多)

  • 协程调度采用最简单的单线程模型

  • 设计之初就没考虑用多线程充分利用多核资源,我们认为直接多部署一些进程就好。

  • 对于一定要把单进程承载做的很高的极少数场景,可以专事专办,做专门的方案即可。

  • 协程采用固定的栈大小

  • 通常几百k就够了(例如256k或者512k),创建协程的时候就预分配好。

  • 这点确实不如growable stack那么高明,但是从实践看也算够了,这样就免去了stack动态增长的工作(从应用编程的视角看,其实C++里我们可能因为无法做指令插入埋点,本来就做不到stack动态增长)。

  • 我们在相邻stack之间加一些写保护page,这样一旦踩了就会 coredump。

  • 同时通过编译选项,限制单层栈大小不能超过某个阈值。

  • 协程调度完全不考虑公平性,全部采用主动handoff策略

    对于某个协程,如果它要持续运行,就任它运行,直到要进行阻塞类操作(典型如RPC调用),才会交出执行权。实际上对于业务来讲,微观层面几十毫秒内哪个协程多占了一点执行权真的无所谓,不用太讲究公平性。假如真的有些协程饿死了,那说明业务都已经过载了(就是时时刻刻都在跑其他协程,cpu100),此时讨论公平也没什么意义了。假如我们真的要做,因为做不到指令插入,只能采用Signals信号中断的方式,在注册的信号处理函数中直接按需切栈。

  • 主协程主控循环tick直接管理协程,协程调度不涉及background thread

  • 网络IO、第三方异步API tick驱动、timer管理、协程创建销毁管理等都是主协程在做。

  • 主控循环中,如果要创建或恢复协程,就任由它去立即执行,一直跑到它阻塞挂起再返回主协程。

协程切换示意图,图注:1、2、5在主协程,3、4在业务协程,主协程和业务协程都在主线程内。

  • 仍可以有基于逻辑分治的多线程

  • 框架不是真的只有一个线程,按功能拆分的日志线程,依然可以存在。

  • 对于一些第三方异步API,如果其tick本身实现不好,导致大量占据了运行时间,也可以分拆线程,然后用队列之类的机制和主线程的主协程交互即可。

  • 对于网络IO也同上。

总之,这种基于逻辑分治做线程拆分的改造都是很简单的,也并不会影响到核心协程调度的机制。


如果有什么疑问,想深入学习,欢迎大家加入极客星球,让我们一起进步,掌握核心技术,既能挣钱又能抗压,挣钱和事业两不误,对星球感兴趣的,点击查看-> 极客星球,公众号回复“优惠卷”,或者扫描下面二维码可以加入。里面还有之前几期的直播分享视频,星球分享的东西都很干货。

同时我每周都会提问几道非常经典的面试题,通过参与这些经典的面试题分析验证,我们可以彻底理解大厂面试的核心知识点,需要深入交流学习同学,可以加入极客星球,和大家一起快速成长:

  • 大厂求职核心原理1v1指导(职位,简历,面试,策略等一条龙优化)

  • 技术问题帮忙分析解答(有专属VIP群)

可以加我微信详细了解。

  • 大厂技术路线

  • 后台开发进阶

  • 开源项目学习

  • 直播交流分享(已经分享了8期,加入星球可以看回放)

  • 技术视野

  • 按需提供经典资料,节约你时间

  • 实战技能分享

- END -


看完一键三连在看转发点赞

是对文章最大的赞赏,极客重生感谢你

推荐阅读

定个目标|建立自己的技术知识体系

大厂后台开发基本功修炼路线和经典资料

个人学习方法分享

你好,这里是极客重生,我是阿荣,大家都叫我荣哥,从华为->外企->到互联网大厂,目前是大厂资深工程师,多次获得五星员工,多年职场经验,技术扎实,专业后端开发和后台架构设计,热爱底层技术,丰富的实战经验,分享技术的本质原理,希望帮助更多人蜕变重生,拿BAT大厂offer,培养高级工程师能力,成为技术专家,实现高薪梦想,期待你的关注!点击蓝字查看我的成长之路

校招/社招/简历/面试技巧/大厂技术栈分析/后端开发进阶/优秀开源项目/直播分享/技术视野/实战高手等, 极客星球希望成为最有技术价值星球,尽最大努力为星球的同学提供面试,跳槽,技术成长帮助!详情查看->极客星球

求点赞,在看,分享三连

从Golang调度器的作者视角探究其设计之道!相关推荐

  1. Golang调度器GPM原理与调度全分析

    第一章 Golang调度器的由来 第二章 Goroutine调度器的GMP模型及设计思想 第三章 Goroutine调度场景过程全图文解析 一.Golang"调度器"的由来? (1 ...

  2. golang源码分析:调度器chan调度

    golang调度机制chan调度 golang的调度策略中,碰见阻塞chan就会将该chan放入到阻塞的g中,然后再等待该chan被唤醒,这是golang调度器策略的主动调度策略之一,其中还有其他的主 ...

  3. Golang的协程调度器原理及GMP设计思想

    一.Golang"调度器"的由来? (1) 单进程时代不需要调度器 我们知道,一切的软件都是跑在操作系统上,真正用来干活(计算)的是CPU.早期的操作系统每个程序就是一个进程,知道 ...

  4. 7种主流案例,告诉你调度器架构设计通用法则(干货!)

    女主宣言 今天小编为大家转载一篇来自DBAplus社群的干货文章,希望能够帮助大家对关于调度器的理解.作者张晨,Strikingly数据平台工程师,算法.分布式系统和函数式编程爱好者.Shanghai ...

  5. 并发问题的解决思路以及Go语言调度器工作原理

    上周的文章<Go并发编程里的数据竞争以及解决之道>最后留下了一个用并发解决的思考题,期间有几位同学留言说了自己的实现思路,也有两位直接私信发代码让我看的,非常感谢几位的积极参与.今天的文章 ...

  6. 上周并发题的解题思路以及介绍Go语言调度器

    上周的文章<Go并发编程里的数据竞争以及解决之道>最后留下了一个用并发解决的思考题,期间有几位同学留言说了自己的实现思路,也有两位直接私信发代码让我看的,非常感谢几位的积极参与.今天的文章 ...

  7. Go GPM 调度器介绍

    Go GPM 调度器介绍 1 简介 ​ 这几天在学习Go的GPM机制,于是就整理了一下收集的资料分享给大家,文章末尾有原文链接.主要介绍了Go在运行时调度器的基本实现逻辑和演变过程. ​ 2 什么是G ...

  8. [转]Golang中goroutine的调度器详解

    Go调度器原理浅析 来源:https://www.douban.com/note/300631999/ goroutine是golang的一大特色,或者可以说是最大的特色吧(据我了解),这篇文章主要翻 ...

  9. golang var 初始化时机_你应该知道的 Go 调度器知识:Go 核心原理 — 协程调度时机...

    点击上方蓝色"Go语言中文网"关注我们,领全套Go资料,每天学习 Go 语言 本文作者:叶不闻 原文链接:https://juejin.im/post/5dafc241f265da ...

最新文章

  1. 固定表头和首行_Excel一步制作斜线表头!还有这些高分Excel表头技巧,看完秒会...
  2. Xamarin iOS教程之显示和编辑文本
  3. C#中String与Datetime
  4. 线性规划 —— matlab
  5. 使用Hyper-V创建虚拟机
  6. JAVA培训—线程同步--卖票问题
  7. python selenium翻页_Selenium翻页的实现方法实例
  8. 【为书豪相亲】单身小姐姐你在哪里,我是书豪,我在等你
  9. android抓包为什么有些数据抓不了?抓包的辛酸历程
  10. Kindle PaperWhite 3 5.8.10越狱成功!
  11. vbs文件放在java工程中如何调用_VBS教程:在 VBScript 中使用对象
  12. 数据分析:单元3 图像的手绘效果实现
  13. 移动硬盘linux双系统,在移动硬盘安装Linux(Ubuntu)双系统
  14. 基于matlab的车牌定位算法设计与实现,原创】基于matlab的汽车牌照识别系统设计与实现...
  15. 真人踩过的坑,告诉你避免自动化测试常犯的10个错误
  16. 该文件包与具有同一名称的现有文件包存在冲突
  17. 巴比特《8问》专访 Conflux 创始人龙凡教授
  18. 计算器表格边框java_表格边框探秘
  19. 在PlatEMO v2.9中增加多模态多目标算法(1)
  20. 微信朋友圈分享链接调用外部浏览器打开指定URL链接是如何实现的

热门文章

  1. Dubbo-Dependency
  2. d3中文案例_D3.js柱状图例子
  3. 200801一阶段1函数封装
  4. Pycharm 2018.2.1-2018.1
  5. Chapter4 Java流程控制之选择结构
  6. 交换算法经常使用的两个数的值
  7. Android4.0图库Gallery2代码分析(二) 数据管理和数据加载
  8. js/jquery判断浏览器的方法总结
  9. 关于Spring的IOC和DI
  10. apache虚拟主机301跳转问题