弄懂goroutine调度原理

939496716 · · 2992 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

[原文地址:https://bingjian-zhu.github.io/2019/09/12/%E5%BC%84%E6%87%82goroutine%E8%B0%83%E5%BA%A6%E5%8E%9F%E7%90%86/](https://bingjian-zhu.github.io/2019/09/12/%E5%BC%84%E6%87%82goroutine%E8%B0%83%E5%BA%A6%E5%8E%9F%E7%90%86/) ### goroutine简介 >golang语言作者Rob Pike说,**“Goroutine是一个与其他goroutines 并发运行在同一地址空间的Go函数或方法。一个运行的程序由一个或更多个goroutine组成。它与线程、协程、进程等不同。它是一个goroutine“**。 <!--more--> * goroutine通过通道来通信,而协程通过让出和恢复操作来通信; * goroutine 通过Golang 的调度器进行调度,而协程通过程序本身调度; 简单的说就是Golang自己实现了协程并叫做goruntine(本文称Go协程),且比协程更强大。 ### goroutine调度原理 上面说到Go协程是通过Golang的调度器进行调度的,其中调度器的线程模型为两级线程模型。 >有关两级线程模型的介绍,可以看[这篇文章](https://bingjian-zhu.github.io/2019/09/11/%E7%BA%BF%E7%A8%8B%E5%AE%9E%E7%8E%B0%E6%A8%A1%E5%9E%8B/) 我们来看下Golang实现的两级线程模型是怎样的。首先要知道这三个字母代表的含义 * **M**:代表内核级的线程 * **P**:全程Processor,代表运行Go协程所需要的资源(上下文环境) * **G**:代表Go协程 ![图一](http://pydjgif25.bkt.clouddn.com/goroutine/2019-9-12/4.png) 我们先看下为实现调度Golang定义了这些数据结构存M,P,G | 名称 | 作用范围 | 描述 | |:--------------------|:----------|:-------------------------| | 全局M列表 | Go的运行时 | 存放所有M的单向链表 | | 全局P列表 | Go的运行时 | 存放所有P的数组 | | 全局G列表 | Go的运行时 | 存放所有G的切片 | | 调度器的空闲M列表 | 调度器 | 存放空闲M的单向链表 | | 调度器的空闲P列表 | 调度器 | 存放空闲P的单向链表 | | 调度器的自由G列表 | 调度器 | 存放自由G的单向链表(有两个) | | 调度器的可运行G队列 | 调度器 | 存放可运行G的队列 | | P的自由G列表 | 本地P | 存放当前P中自由G的单向链表 | | P的可运行G队列 | 本地P | 存放当前P中可运行G的队列 | 然后从上往下解析Go的两级线程模型图 (1)M和内核线程之间是一对一的关系,一个M在其生命周期中,只会和一个内核线程关联,所以不会出现对内核线程的频繁切换; >Golang的运行时执行系统监控和垃圾回收等任务时候会导致创建M,M空闲时不会被销毁,而是放到一个`调度器的空闲M列表`中,等待与P关联,M默认数量为10000 (2)P和M之间是多对多的关系,P和G之间是一对多的关系,他们的关联是易变的,由Golang的调度器完成调度; >Golang的运行时按规则调度,让P和不同的M建立或断开关联,使得P中的G能够及时获得运行时机 (3)P的数量默认为CPU总核心数,最大为256,当P没有可运行的G时候(P的可运行G队列为空),P会被放到`调度器的空闲P列表`中,等待M与它关联; >P有可能会被销毁,如运行时用runtime.GOMAXPROCS把P的数量从32降到16时,剩余16个会被销毁,它们原来的G会先转到调度器`可运行的G队列`和`自由G列表` (4)每个P中有`可运行的G队列`(如图中最下面的那行G)和`自由G列表`(图中未画出来),当G的代码执行完后,该G不会被销毁,而是被放到`P的自由G列表`或`调度器的自由G列表`。如果程序新建了Go协程,调度器会在自由G列表中取一个G,然后把Go协程的函数赋值到G中(如果自由G列表为空,就创建一个G); >可见Golang调度器在调度时很大程度复用了M,P,G (5)在Go程序初始化后,调度器首先进行一轮调度,此时用M去搜索可运行的G。其中我们的main函数也是一个G,找到可运行的G后就执行它; >至于怎么找可运行的G呢?答案是到处找,想尽办法找(这里只列出一部分地方)。 >* 从`本地P的可运行的G队列`找 >* 从`调度器的可运行的G队列`找 >* 从`其他P的可运行的G队列`找 (6)`P的可运行G队列`最大只能存放长度为256的G,当队列满后,调度器会把一半的G转到`调度器的可运行G队列`。 ### 系统监控 上面大概描述了关于goroutine调度的流程。现在还存在一个问题,那就是当Go协程很多(并发量大)时候,显然G是不能一直执行下去的,因为也需要把执行机会留给其他的G。此时Golang运行时的系统监控就起作用了。 一般情况,当G运行时间超过10ms后,该G就会被系统告知需要停止了,让其他G运行。(这里情况比较复杂,并不能确保每个G都能被公平执行) >以下特殊情况该G不需要停止 >* P的可运行G队列为空(没有其他G可运行) >* 有空闲的M在寻找可运行的G(没有其他G可运行) >* 空闲的P(还有P闲着) ### 总结 Golang以两级线程实现模型,自己实现goruntine和调度器,优势在于并行和非常低的资源使用。 >**主要体现:** >* 内存消耗方面(每个Go协程占的内存远小于线程占的内存) >* 切换(调度)开销方面 >* 线程切换涉及模式切换(从用户态切换到内核态) 此外,Go协程执行任务完成的顺序并不都是按我们预期的那样(程序不加以控制的情况下),特别在一些耗时较长的任务中。且每个Go协程执行的时间也不是绝对公平的。 如有错误地方,还请狂喷!

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2992 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传