转自  http://wangzhezhe.github.io/blog/2016/02/17/golang-scheduler/

基本上是网上相关文章的梳理,初衷主要是想了解下golang中的goroutine到底是怎么回事,以及相关的起源和概念。后来发现本质上应该是对于golang scheduler的理解,因为goroutine是golang scheduler实现的一个重要模块。这一篇入门吧,基本理解到还行,如果想深入细致了解还是应该看源码,就像参考的那些比较好的链接中的那样。

补充 同步 异步 阻塞 非阻塞

同步与异步区别,主要关注的是消息通信机制

所谓同步调用 就是由调用者主动等待这个调用的结果。发出一个调用,在没有得到结果之前,该调用就不返回。一旦调用返回,就得到返回值了。

所谓异步调用 调用在发出之后,这个调用结果就直接返回了。当一个异步调用过程在发出之后,调用者不会立即得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或者通过函数回调来处理这个调用。

阻塞与非阻塞 关注的是:程序在等待调用结果(消息 返回值)时的状态

阻塞调用 是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用 是指,在不能立刻得到结果之前,该调用不会阻塞当前线程,当前线程还会继续执行下去。

注意!!! 阻塞与非阻塞与是否同步和是否异步无关。

进程 线程 协程

基本理解

大致上看有这么几个区别:

进程:独立的栈空间,独立的堆空间,进程之间调度由os完成。

线程:独立的栈空间,共享堆空间,内核线程之间调度由os完成。

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。

这个帖子中排行第一的评论整理得比较通俗,便于理解入门,先整理如下:

首先是并发的起源,最初的动机就是想在宏观上,让多个程序能在同一时间执行,之后就是cpu分片,程序内部多个独立的逻辑流,宏观上多个逻辑流是一同执行的,当然也可以是多个cpu并行。

进一步的问题,多个逻辑流之间的切换怎么办,我的逻辑a计算到一半,逻辑b进来,那么逻辑a的中间结果怎么保存?所以同一个cpu中的多个并发执行的逻辑自然需要进行上下文切换。于是就需要进程的概念了,通过虚拟内存,进程表,等等内容来管理程序的运行和切换。

硬件进一步发展,一台电脑多个cpu于是一个cpu跑一个进程,这个就是并行了,是从时间意义上的完全的共同执行。

由于涉及到并行问题,那自然会有调度问题,怎么调度才能让cpu的利用率更高?这个就是内核应该程序要考虑的事。实质上就是某种权衡把,因为调度也是要开销的,所以就看这种调度是否值得去做。

课本上还是讲得挺明白,由于为了满足上述并发的需求,进程最为一个可拥有资源的,可独立调度和分派的基本单位而存在,但是进程的创建,撤销,切换其实都是需要不少开销的,如果进程切换过于频繁,系统资源就会被频繁开销所占去。于是控制粒度再进一步细化,即把“拥有资源”和“独立调度”两个属性分开来。线程仅仅拥有很小的一部分资源,共享线程的资源。其开销显著地小于进程的切换,操作系统的书里,都说的很细致,就不再赘述。

把调度的那部分功能从内核中拿出来,在进程中自己去实现一个逻辑流调度的功能,这样既可以实现并发的优势,又可以避免反复的系统调用,减少线程切换造成的开销,这就叫做用户态线程,相当于是调度功能的更细粒度的实现。

用户态线程需要考虑的问题:1、遇到阻塞式I/O会导致整个进程被挂起 2、由于缺乏时钟中断(具体查看相关内容 时钟中断的时候cpu可以用来进行进程切换)如果一种实现使得每个线程需要自己通过调用某个方法,主动交出控制权。那么我们就称这种用户态线程是协作式的,就是所谓的协程

具体在这篇文章可以看到,通过这个文章应该清楚这几个问题:协程是如何被提出的,为何开始的时候没有被普遍采用,以及后来又是如何兴起的。以及对于“协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制”的理解,以及协程在不同语言中的大概的实现方式。

关于采用协程的优势,参考这里,原文中也列出了python实现协程思想的一个模型。

  • 协程之间的切换是由程序自身控制的,没有线程切换的额外开销,和多线程相比,线程数目越多,协程的性能优势就越明显。
  • 不需要多线程的锁机制(因为只有一个线程),怎么利用多核CPU?最简单的方式是多进程+协程,比如在Golang程序开始的时候往往需要制定下并发进程的数目,类似这样:
1
2
3
4
5
6
if *maxProcs < 1 {
numProcs = runtime.NumCPU()
} else {
numProcs = *maxProcs
}
runtime.GOMAXPROCS(numProcs)

golang的协程模型的实现

其实核心就是调度器应该如何实现,这的确是一个比较复杂的问题。这里主要参考这个。其中的内容是在Golang的1.1版本的基础上经行的分析,后面的版本可能有些地方已经进行了改进。

Golang runtime的时候 scheduler 需要完成哪些工作?

首先要明确下,我们为什么需要scheduler。既然os本身可以调度线程,为何还需要在用户空间实现一个调度器?

POSIX Thread API 实际上是对于已经存在的Unix process model 的一个逻辑扩展。这使得在控制threads的时候,用些地方和控制processes比较类似。threads可以有它们自己的signal mask,有cpu亲和力(CPU affinity),可以被cgroup机制进行控制,并且可以查询到它们都使用了哪些资源。所有的对于threads的这些额外的控制特性都会增加额外的开销。对于Golang使用goroutine来说,这些特性是不需要的。

从Golang本身的角度来说,如果让os去做schedule,这样粒度不是很细,schedule的时机选择也并不是最佳的,因为os无法知道golang在runtime时候的一些更进一步的信息。比如,Golang的GC在启动的时候需要保证以下两个方面

1、所有的threads都停止

2、memory必须达到一致的状态(这里的memory consistency到底指的什么?)这就需要golang在runtime的时候等到所有的运行的threads都到达memory consistent的时候才能启动GC。

可以想象到,如多有许多threads在某种程度上“随机”的时间点被进行调度(os的方式下 根据时钟中断?),就会经常需要去等待这些threads,等它们达到一个consistent state。如果是golang 自身实现的调度器,它可以在所有threads达到memory consistency的状态的时候才决定进行调度,因此在调度时机的选取上,这样更为高效。即是说,当我们准备进行Garbage Collection的时候,只需要等待那些正在被运行的进程停下来就可以了。

(gopher china 2016 Dave slide )每个goroutine至少会占用2k的内存空间,2048 * 1,000,000 goroutines == 2Gb,也就是说,2G内存的机器,最多可以承担100万的goroutine,所以在每次使用go关键字的时候,明确goroutine会怎样退出,如果无法明确的回答这个问题,可能会导致潜在的内存泄露,当然,有些goroutine会一直运行,直到main函数运行结束,这也是gc优化的一个技巧。

所以:

Never start a goroutine without knowing how it will stop

Golang 中的调度器模型基本介绍

通常情况下有三种线程模型:

  • N:1 N个用户级线程以及一个内核级线程。这种模式下,用户级线程之间的切换可以很快,但是不能很好的利用多核的优势。
  • 1:1 一个用户级线程对应一个内核级线程。可以利用多核的优势,但是线程切换比较慢,因为需系统调用,要进行trap操作。

Golang中的调度器采用 M:N 的方式。既要利用多核cpu系统的特性,同时还要增强上下文切换的速度。缺点就是,这会使得调度器的实现变得复杂。

可以看到golang scheduler中包含以下的基本元素:

三角形M 代表一个os thread,这个thread被os管理,工作方式就像通常的POSIX thread那样。

圆形G 代表一个goroutine,它有自己的stack,instruction pointer,程序计数器,以及它所在的M等信息,以及一些调度goroutine所必须的一些资源,这些信息就是goroutine要放弃cpu的时候所需要保存的信息,比如正在阻塞的channel等等,下次被调度到的时候,这些信息要被重新load到对应的cpu寄存器中。

矩形P 代表一个用于调度的上下文context,从理解上,可以认为这是一个运行在单独thread中的scheduler或者理解成一个局部的Processor处理器。这个组件很关键,是从N:1调度器到M:N调度的器的关键部分。

上图是一个一般情况,可以看到有两个内核thread(M),每一个内核thread都持有一个context(P),每一个内核线程还运行着一个goroutine,为了运行goroutines,内核线程必须要持有一个context。

context的数量是在goroutine运行的时候由 GOMAXPROCS 这个环境变量设置进去的,通过runtime的GOMAXPROCS()函数可以设置这个值。通常情况下,这个值在程序运行的过程中是不变的,即使说,GOMAXPROCS 是实际负责运行Golang代码的组件,可以通过其数目来调整实际的GO process的数目。(GOMAXPROCS 就是图中的那个P 到底是什么东西?理解上感觉像是个容器一样?里面装着的golang 实际运行代码可以更换 但是离不开这个运行的环境)

上图中灰色标记的goroutine并没有在运行,它们是准备被调度的(not running but ready to scheduled)。它们被分配在一个个的list中,这些list叫做runqueues。新的goroutine会被添加到runqueues的尾部。在调度点的时候,当context需要运行一个goroutine,一个goroutine就会从list中弹出来,之后设置好对应的stack以及instruction pointer之后这个goroutine就开始运行了。

为了减少并发冲突,每个context都有它们自己的一个runqueue(旧的版本貌似只有一个全局的runqueue),当然以上情况只是最一般的情况,实际比这要更复杂一些。

发生系统调用syscall的情况

为什么必须要一个context(即使图中的P)?为何不能直接让requeues在thread上运行?当某个正在running的线程陷入阻塞的时候,这个p可以临时转移到新的线程上去。

比如某个gourutine正在进行系统调用,因为线程不能一边执行代码,一边阻塞在系统调用上,所以p可以带着gourutine转移到其他的os线程上去,如下图

这里我们可以看到,原来的内核线程M0放弃了它本身的context,M0陷入了阻塞,之后这个context又与新的内核线程M1绑定在了一起。调度器可以保证有足够的thread来运行这些context。原来的M0仍然持有者之前的那个goroutine,因为本质上来说,它还是执行着的,虽然被os阻塞。

当syscall的结果返回,M0必须要想办法持有一个context来运行之前的goroutine。因为按照之前分析的,如果goroutine想要运行,必须要有一个context作为支撑。通常的方式是steal一个context过来,如果暂时没有可用的context,当前的这个goroutine可能会被放到全局的runqueue中,这个thread会把自己放到thread cache中变成sleep状态。

当context的local runqueue中已经没有goroutine了,它们可能会从global runqueue中获取一个goroutine过来。context也可能会周期性地检查runqueue看其中是否还有goroutine。否则全局runqueue中的goroutine可能会永远无法运行,会被"饿死"(context的local runqueue一直有goroutine在运行)。

对于syscall的处理方式决定了golang在运行起来的时候,本身就是多线程的,即使GOMAXPROCS的数目被设置为1,因为在发生syscall的时候,p会在一个新启动的thread上继续运行。可以知道在golang中不会直接让用户去创建一个os层面的thread,这个工作完全是由runtime根据实际情况来决定何时创建thread。用户能创建的仅仅只goroutine,让用户管理的资源越少,操作的东西越简单,用户就会越happy。

steal work

work stealing的策略应该也是一种调度算法,大致上是这样:如果context上的goroutine数目不平衡,golang的调度器也会进行相应的处理,就是所谓的"steal work"。一种情况是从全局requeue中获取goroutine继续运行,另一种情况是从其他的context中steal一些过来,比如从其他的context的local requeue中steal一半数目的goroutine过来,就像下图中的这样。这样可以保证每个context都有一些工作需要完成,这样也可以保证所有的threads都发挥出了它们最大的性能。

调度时间点的选取

这个参考的这篇,通过之前的分析可以看到,采用自己实现的调度器的一个重要方面就是由runtime自己决定调度的时机,那么具体情况是怎么样的?

这里罗列写可能的基本情况:

  • runtime.park函数被调用,可能会使得goroutine变为waiting状态,放弃cpu。channel读写操作的时候,定时器中,网络poll都可能会调用runtime.park函数。
  • runtime·gosched函数也可以让当前goroutine放弃cpu,但和park完全不同;gosched是将goroutine设置为runnable状态,然后放入到调度器全局等待队列(global runqueue)。
  • 有些系统调用会触发重新调度。比如之前提到的syscall的情况,runtime的时候会有一个goroutine负责系统监控,对goroutine进行扫描,如果发现某一个goroutine处在syscall的状态下,就会像前面分析的那样,创建一个新的M,把那个P抢过来,让这个P开始运行goroutine。等到系统调用结束,原来的goroutine发现自己这边没有P,无法执行,就会被放到全局的requeue上,之后原先的线程也会变为sleep的状态。

总结

threads粒度太粗,会有诸多额外开销 -> goroutine不需要这些开销,它们需要更细粒度的控制 -> golang中scheduler的模型(几种线程模型 M P G 含义 )-> (M P G 优点) 某个G陷入阻塞的时候 P可以带着其他的G转移到其他的M上,当原先的G系统调用完成以后,会从另外一个地方steal一个P回来 -> 提高资源利用率

要通过golang scheduler的基本模型理解其本质的东西,即最终目的是要使得所有资源的利用率最大,使用到其他地方,应该也要有些启发。比如k8s的调度的策略。

实际实现当然是一个很复杂的过程,比如这篇从更细节的层面分析了golang的调度器,也比较有参考价值。

感觉要想深入了解,还是应该把这些本质的东西弄清楚一点。比较推荐这个可以按照大牛的思路一块块地了解相关内容。

还有一个深入了解语言层面的方式,比如好多人会发帖,说这个语言怎么样,有什么的缺陷,等等,可以顺藤摸瓜,顺着这些人的思路走下去,看看到底细节上是怎样的,这样也能提升好多。

比如可以参考这个

参考资料

golang与jvm中并发模型的探讨

http://www.nyankosama.com/2015/04/03/java-goroutine/

协程的一些介绍

http://www.cnblogs.com/wonderKK/p/4062591.html http://blog.youxu.info/2014/12/04/coroutine/

zhihu相关帖子

http://www.zhihu.com/question/20511233

https://www.zhihu.com/question/20862617

http://www.zhihu.com/question/32218874

协程的过去现在未来(从cobol到协程的整个发展演变 比较经典) http://www.tuicool.com/articles/BNvUfeb

http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/0013868328689835ecd883d910145dfa8227b539725e5ed000

大牛的博客 里面有许多关于golang的文章 比如gc之类的

http://morsmachine.dk/

牛人的golang学习笔记(从源码角度分析)

https://github.com/qyuhen/book

关于golang的调度器(也写得比较通俗)

http://skoo.me/go/2013/11/29/golang-schedule/

Posted by wangzhe Feb 17th, 2016 3:53 pm  golang

以goroutine为例看协程的相关概念的更多相关文章

  1. 从Erlang进程看协程思想

    从Erlang进程看协程思想 多核慢慢火了以后,协程类编程也开始越来越火了.比较有代表性的有Go的goroutine.Erlang的Erlang进程.Scala的actor.windows下的fibr ...

  2. go语言之进阶篇创建goroutine协程

    1.goroutine是什么 goroutine是Go并行设计的核心.goroutine说到底其实就是协程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现 ...

  3. python 多线程、多进程、协程性能对比(以爬虫为例)

    基本配置:阿里云服务器低配,单核2G内存 首先是看协程的效果: import requests import lxml.html as HTML import sys import time impo ...

  4. [golang note] 协程基础

    协程概念 √ 协程通常称为coroutine,在golang中称为goroutine. √ 协程本质上是一种用户态线程,它不需要操作系统来进行抢占式调度,在实际实现中寄存在线程之中. √ 协程系统开销 ...

  5. golang 进程、线程、协程 简介

    https://www.cnblogs.com/shenguanpu/archive/2013/05/05/3060616.html https://studygolang.com/articles/ ...

  6. Python多任务之协程

    前言 协程的核心点在于协程的使用,即只需要了解怎么使用协程即可:但如果你想了解协程是怎么实现的,就需要了解依次了解可迭代,迭代器,生成器了: 如果你只想看协程的使用,那么只需要看第一部分内容就行了:如 ...

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

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

  8. 写个百万级别full-stack小型协程库——原理介绍

    其实说什么百万千万级别都是虚的,下面给出实现原理和测试结果,原理很简单,我就不上图了: 原理:为了简单明了,只支持单线程,每个协程共享一个4K的空间(你可以用堆,用匿名内存映射或者直接开个数组也都是可 ...

  9. 实现一个简单的C++协程库

    之前看协程相关的东西时,曾一念而过想着怎么自己来实现一个给 C++ 用,但在保存现场恢复现场之类的细节上被自己的想法吓住,也没有深入去研究,后面一丢开就忘了.近来微博上看人在讨论怎么实现一个 user ...

随机推荐

  1. 是男人就过 8 题--Pony.AI A AStringGame

    链接:https://www.nowcoder.com/acm/contest/92/A来源:牛客网 AStringGame 时间限制:C/C++ 2秒,其他语言4秒 空间限制:C/C++ 26214 ...

  2. Linux Shell系列教程之(十)Shell for循环

    本文是Linux Shell系列教程的第(十)篇,更多Linux Shell教程请看:Linux Shell系列教程 基本任何语言都有自己的循环语句,Shell当然也不例外,今天就为大家介绍下Shel ...

  3. localStorage的用法

    1.在HTML5中,本地存储是一个window的属性,包括localStorage和sessionStorage,前者是一直存在本地的,后者是伴随着session,窗口一旦关闭就消失了.二者用法完全相 ...

  4. NOJ——1672剪绳子(博弈)

    [1672] 剪绳子 时间限制: 500 ms 内存限制: 65535 K 问题描述 已知长度为n的线圈,两人依次截取1~m的长度,n, m为整数,不能取者为输. 输入 输入n, m:( 0 < ...

  5. [CODEVS1917] 深海机器人问题(最小费用最大流)

    传送门 [问题分析] 最大费用最大流问题. [建模方法] 把网格中每个位置抽象成网络中一个节点,建立附加源S汇T. 1.对于每个顶点i,j为i东边或南边相邻的一个节点,连接节点i与节点j一条容量为1, ...

  6. <定时主库导出/备库导入>

    1.设置定时任务时间及所需要的dmp文件路径 [mm1@localhost ~]$ crontab -e 0 0 * * *  sh /home/mm1/exp_table.sh  2>& ...

  7. greenplum /postgres 登陆以及创建修改用户密码

    1.greenplum 启动 bin目录下的gpstart  ,-m为只启动master 2.greenplum 启动之后,通过postgresql登陆 登陆命令:PGOPTIONS="-c ...

  8. linux监控平台搭建-磁盘

    linux监控平台搭建-磁盘 磁盘:随着大数据快速发展.人工智能.自动化.云平台.数据量指数的增长.磁盘的使用量也在增长.目前的机器基本上采用SSD或者SATA盘,一直有人比较那个好.会不会使用时间短 ...

  9. bzoj 3208 花神的秒题计划I

    bzoj 3208 花神的秒题计划I Description 背景[backboard]: Memphis等一群蒟蒻出题中,花神凑过来秒题-- 描述[discribe]: 花花山峰峦起伏,峰顶常年被雪 ...

  10. MVC中使用ajax传递json数组

    解决方法 去www.json.org下载JSON2.js再调用JSON.stringify(JSONData)将JSON对象转化为JSON串. var people = [{ "UserNa ...