goroutine是golang的一大特色,或者可以说是最大的特色吧(据我了解),这篇文章主要翻译自Morsing的[这篇博客](http://morsmachine.dk/go-scheduler),我读这篇文章的时候不只是赞叹调度器设计的精巧,而且被Unix内核设计思想的影响和辐射所震撼,感觉好多好东西都带着它的影子。

绪论(Introduction)
---------------------
Go 1.1最大的特色之一就是这个新的调度器,由Dmitry Vyukov贡献。新调度器让并行的Go程序获得了一个动态的性能增长,针对它我不能再做点更好的工作了,我觉得我还是为它写点什么吧。

这篇博客里面大多数东西都已经被包含在了[原始设计文档](https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw)中了,这个文档的内容相当广泛,但是过于技术化了。

关于新调度器,你所需要知道的都在那个设计文档中,但是我这篇博客有图片,所以更加清晰易懂。

带调度器的Go runtime需要什么?(What does the Go runtime need with a scheduler?)
-------------------------------------------------------------------------------
但是在我们开始看新调度器之前,我们需要理解为什么需要调度器。为什么既然操作系统能为我们调度线程了,我们又创造了一个用户空间调度器?

POSIX线程API是对现有Unix进程模型的一个非常大的逻辑扩展,而且线程获得了非常多的跟进程相同的控制。比如,线程有它自己的信号掩码,线程能够被赋予CPU affinity功能(就是指定线程只能在某个CPU上运行),线程能被添加到[cgroups](http://en.wikipedia.org/wiki/Cgroup)中,线程所用到的资源也可以被查询到。所有的这些控制增大了Go程序使用gorroutines时根本不需要的特性(features)的开销,当你的程序有100,000个线程的时候,这些开销会急剧增长。

另外一个问题是,基于Go模型,操作系统不能给出特别好的决策。比如,当运行一次垃圾收集的时候,Go的垃圾收集器要求所有线程都被停止而且要求内存要处于一致状态(consistent state)。这个涉及到要等待全部运行时线程(running threads)到达一个点(point),我们事先知道在这个地方内存是一致的。

当很多被调度的线程分散在随机的点(random point)上的时候,结果就是你不得不等待他们中的大多数到达一致状态。Go调度器能够作出这样的决策,就是只在内存保持一致的点上进行调度。这就意味着,当我们为垃圾收集而停止的时候,我们只须等待在一个CPU核(CPU core)上处于活跃运行状态的线程即可。

来看看里面的各个角色(Our Cast of Characters)
-----------------------------------------
目前有三个常见的线程模型。一个是N:1的,即多个用户空间线程运行在一个OS线程上。这个模型可以很快的进行上下文切换,但是不能利用多核系统(multi-core systems)的优势。另一个模型是1:1的,即可执行程序的一个线程匹配一个OS线程。这个模型能够利用机器上的所有核心的优势,但是上下文切换非常慢,因为它不得不陷入OS(trap through the OS)。

Go试图通过M:N的调度器去获取这两个世界的全部优势。它在任意数目的OS线程上调用任意数目的goroutines。你可以快速进行上下文切换,并且还能利用你系统上所有的核心的优势。这个模型主要的缺点是它增加了调度器的复杂性。

为了完成调度任务,Go调度器使用了三个实体:

 

三角形表示OS线程,它是由OS管理的可执行程序的一个线程,而且工作起来特别像你的标准POSIX线程。在运行时代码里,它被成为M,即机器(machine)。

圆形表示一个goroutine。它包括栈、指令指针以及对于调用goroutines很重要的其它信息,比如阻塞它的任何channel。在可执行代码里,它被称为G。

矩形表示用于调用的上下文。你可以把它看作在一个单线程上运行代码的调度器的一个本地化版本。它是让我们从N:1调度器转到M:N调度器的重要部分。在运行时代码里,它被叫做P,即处理器(processor)。这部分后面会多说点。

 

我们可以从上面的图里看到两个线程(M),每个线程都拥有一个上下文(P),每个线程都正在运行一个goroutine(G)。为了运行goroutines,一个线程必须拥有一个上下文。

上下文的数目在启动时被设置为环境变量GOMAXPROCS的值或者通过运行时函数GOMAXPROCS()来设置。通常,在你的程序执行时它不会发生变化。上下文的数目被固定的意思是,只有GOMAXPROCS个上下文正在任意点上运行Go代码。我们可以使用GOMAXPROCS调整Go进程的调用使其适合于一个单独的计算机,比如一个4核的PC中可以在4个线程上运行Go代码。

外部的灰色goroutines没在运行,但是已经准备好被调度了。它们被安排成一个叫做runqueue的列表。当一个goroutine执行一个go 语句的时候,goroutine就被添加到runqueue的末端。一旦一个上下文已经运行一个goroutine到了一个点上,它就会把一个goroutine从它的runqueue给pop出来,设置栈和指令指针并且开始运行这个goroutine。

为了降低mutex竞争,每一个上下文都有它自己的runqueue。Go调度器曾经的一个版本只有一个通过mutex来保护的全局runqueue,线程们经常被阻塞来等待mutex被解除阻塞。当你有许多32核的机器而且想尽可能地压榨它们的性能时,情况就会变得相当坏。

只要所有的上下文都有goroutines要运行,调度器就能在一个稳定的状态下保持调度。但是有几个你能改变的场景。

你打算(系统)调用谁?(Who you gonna (sys)call?)
------------------------------------------------------
你现在可能想知道,为什么一定要有上下文?我们能不能丢掉上下文而仅仅把runqueue放到线程上?不尽然。`我们用上下文的原因是如果正在运行的线程因为某种原因需要阻塞的时候,我们可以把这些上下文移交给其它线程`。

我们需要阻塞的一个例子是,当我们需要调用一个系统调用的时候。因为一个线程不能既执行代码同时又阻塞到一个系统调用上,我们需要移交对应于这个线程的上下文以让这个上下文保持调度。

 

从上图我们能够看出,一个线程放弃了它的上下文以让另外的线程可以运行它。调度器确保有足够的线程来运行所有的上下文。上图中的M1 可能仅仅为了让它处理图中的系统调用而被创建出来,或者它可能来自一个线程池(thread cache)。这个处于系统调用中的线程将会保持在这个导致系统调用的goroutine上,因为从技术上来说,它仍然在执行,虽然阻塞在OS里了。

当这个系统调用返回的时候,这个线程必须尝试获取一个上下文来运行这个返回的goroutine,操作的正常模式是从其它所有线程中的其中一个线程中“偷”一个上下文。如果“偷盗”不成功,它就会把它的goroutine放到一个全局runqueue中,然后把自己放到线程池中或者转入睡眠状态。

这个全局runqueue是各个上下文在运行完自己的本地runqueue后用来获取新goroutine的地方。上下文也会周期性的检查这个全局runqueue上的goroutine,否则,全局runqueue上的goroutines可能得不到执行而饿死。

`Go程序要在多线程上运行的原因就是因为要处理系统调用,哪怕GOMAXPROCS等于1`。运行时(runtime)使用调用系统调用的goroutines,而不是线程。

盗取工作(Stealing work)
-----------------------------
系统的稳定状态改变的另外一个方法是,当一个上下文运行完要被调度的所有goroutines的时候。如果各个上下文的runqueue里的工作的数目不均衡,改变就会发生了,否则会导致一个上下文在执行完它的runqueue后就会结束,尽管系统中仍然有许多工作要执行。所以为了保持运行Go代码,一个上下文能够从全局runqueue中获取goroutines,但是如果全局runqueue中也没有goroutines了,那么上下文就不得不从其它地方获取goroutines了。

 

这个“其它地方”指的是其它上下文!当一个上下文完成自己的任务后,它就会尝试“盗取”另一个上下文runqueue中工作量的一半。这将确保每个上下文总是有活干,然后反过来确保所有线程尽可能处于最大负荷。

下一步走向何方?(Where to go?)
--------------------------------------
关于调度器还有许多细节,像cgo线程、LockOSThread()函数以及与网络poller的整合。这些已经超过这篇文章的要探讨的范围了,但是仍然值得去研究。以后我会针对这些再写点文章。在Go运行时库里,仍然有大量有意思的创建工作要做。

By Daniel Morsing

(end)

转载地址:https://www.douban.com/note/300631999/

【转】Go调度器原理浅析的更多相关文章

  1. Golang/Go goroutine调度器原理/实现【原】

    Go语言在2016年再次拿下TIBOE年度编程语言称号,这充分证明了Go语言这几年在全世界范围内的受欢迎程度.如果要对世界范围内的gopher发起一次“你究竟喜欢Go的哪一点”的调查,我相信很多Gop ...

  2. Kubernetes集群调度器原理剖析及思考

    简述 云环境或者计算仓库级别(将整个数据中心当做单个计算池)的集群管理系统通常会定义出工作负载的规范,并使用调度器将工作负载放置到集群恰当的位置.好的调度器可以让集群的工作处理更高效,同时提高资源利用 ...

  3. cocos2d-x调度器原理

    程序运行后每达到一帧的时间间隔就会执行一次mainLoop void CCDisplayLinkDirector::mainLoop(void) { //判断是否需要释放CCDirector,通常游戏 ...

  4. IO调度器原理介绍

    IO调度器(IO Scheduler)是操作系统用来决定块设备上IO操作提交顺序的方法.存在的目的有两个,一是提高IO吞吐量,二是降低IO响应时间.然而IO吞吐量和IO响应时间往往是矛盾的,为了尽量平 ...

  5. Erlang/OTP 17.0-rc1 新引入的"脏调度器"浅析

    最近在做一些和 NIF 有关的事情,看到 OTP 团队发布的 17 rc1 引入了一个新的特性“脏调度器”,为的是解决 NIF 运行时间过长耗死调度器的问题.本文首先简单介绍脏调度器机制的用法,然后简 ...

  6. TKE 用户故事 | 作业帮 Kubernetes 原生调度器优化实践

    作者 吕亚霖,2019年加入作业帮,作业帮架构研发负责人,在作业帮期间主导了云原生架构演进.推动实施容器化改造.服务治理.GO微服务框架.DevOps的落地实践. 简介 调度系统的本质是为计算服务/任 ...

  7. MapReduce多用户任务调度器——容量调度器(Capacity Scheduler)原理和源码研究

    前言:为了研究需要,将Capacity Scheduler和Fair Scheduler的原理和代码进行学习,用两篇文章作为记录.如有理解错误之处,欢迎批评指正. 容量调度器(Capacity Sch ...

  8. Java并发编程原理与实战三十八:多线程调度器(ScheduledThreadPoolExecutor)

    在前面介绍了java的多线程的基本原理信息:线程池的原理与使用 本文对这个java本身的线程池的调度器做一个简单扩展,如果还没读过上一篇文章,建议读一下,因为这是调度器的核心组件部分. 我们如果要用j ...

  9. Golang调度器GMP原理与调度全分析(转 侵 删)

    该文章主要详细具体的介绍Goroutine调度器过程及原理,包括如下几个章节. 第一章 Golang调度器的由来 第二章 Goroutine调度器的GMP模型及设计思想 第三章 Goroutine调度 ...

随机推荐

  1. “无法启动IIS Express Web服务器”的解决办法

    “无法启动IIS Express Web服务器”的解决办法 听语音 原创 | 浏览:259 | 更新:2019-07-15 13:02 1 2 3 4 5 6 7 分步阅读 在使用visual stu ...

  2. Python - Django - 中间件 process_response

    process_response 函数是执行完 views.py 后执行的函数 process_response 函数有两个参数,一个是 request,一个是 response,response 是 ...

  3. mac ffmpeg mediainfo视频压缩

    最近在开发官网,官网上放了一些视频,但是本宝宝拿到的都是100多.200多.300多兆的原视频,怎么把他们变成统统20兆呢?宝宝用了如下方法,很好用哟- 1.安装视频压缩工具下载ffmpeg brew ...

  4. javascript (0, obj.prop)()的用法

    我第一次看到这种奇怪的用法是在babel的源码中, 其实它的原理就是使得在prop这个方法里无法获取this, 从而无法对类中的其他变量或方法做操作. obj.prop() 这是一个方法调用, pro ...

  5. 移动端自适应rem的设置

    一般所熟知的css样式大小单位有px,em. px:精确地描述元素大小,不随屏幕大小的变化而变化: em:相对于父元素的大小,如果有层层嵌套的话会导致这个值非常大或者非常小. 在写移动端页面的时候为了 ...

  6. [转帖]Hyperledger Fabric 学习一:简介

    Hyperledger Fabric 学习一:简介 https://www.jianshu.com/p/f971858b70f3?utm_campaign=maleskine&utm_cont ...

  7. [转帖]Linux超级用户root口令忘记怎么办?

    Linux超级用户root口令忘记怎么办? 2010-05-10 12:15:00 monkey_d_meng 阅读数 5535  收藏 更多 分类专栏: Linux   版权声明:本文为博主原创文章 ...

  8. Python实现树

    树 (tree) 是一种非常高效的非线性存储结构.树,可以很形象的理解,有根,有叶子,对应在数据结构中就是根节点.叶子节点,同一层的叶子叫兄弟节点,邻近不同层的叫父子节点,非常好理解. 注:定义来自百 ...

  9. gtest入门

    介绍 gtest是谷歌开发的用来做C++单元测试的测试框架 基本概念 使用gtest,你就需要写断言(assertions),用来检查一个表达式是否为true.断言的结果有三个:正确.非致命错误.致命 ...

  10. 深度学习-Wasserstein GAN论文理解笔记

    GAN存在问题 训练困难,G和D多次尝试没有稳定性,Loss无法知道能否优化,生成样本单一,改进方案靠暴力尝试 WGAN GAN的Loss函数选择不合适,使模型容易面临梯度消失,梯度不稳定,优化目标不 ...