设为首页 收藏本站
查看: 1029|回复: 0

[经验分享] Golang源码探索(二) 协程的实现原理

[复制链接]

尚未签到

发表于 2018-9-20 12:48:34 | 显示全部楼层 |阅读模式
  Golang最大的特色可以说是协程(goroutine)了, 协程让本来很复杂的异步编程变得简单, 让程序员不再需要面对回调地狱,
  虽然现在引入了协程的语言越来越多, 但go中的协程仍然是实现的是最彻底的.
  这篇文章将通过分析golang的源代码来讲解协程的实现原理.
  这个系列分析的golang源代码是Google官方的实现的1.9.2版本, 不适用于其他版本和gccgo等其他实现,
  运行环境是Ubuntu 16.04 LTS 64bit.

核心概念
  要理解协程的实现, 首先需要了解go中的三个非常重要的概念, 它们分别是G, MP,
  没有看过golang源代码的可能会对它们感到陌生, 这三项是协程最主要的组成部分, 它们在golang的源代码中无处不在.

G (goroutine)
  G是goroutine的头文字, goroutine可以解释为受管理的轻量线程, goroutine使用go关键词创建.
  举例来说, func main() { go other() }, 这段代码创建了两个goroutine,
  一个是main, 另一个是other, 注意main本身也是一个goroutine.
  goroutine的新建, 休眠, 恢复, 停止都受到go运行时的管理.
  goroutine执行异步操作时会进入休眠状态, 待操作完成后再恢复, 无需占用系统线程,
  goroutine新建或恢复时会添加到运行队列, 等待M取出并运行.

M (machine)
  M是machine的头文字, 在当前版本的golang中等同于系统线程.
  M可以运行两种代码:


  • go代码, 即goroutine, M运行go代码需要一个P
  • 原生代码, 例如阻塞的syscall, M运行原生代码不需要P
  M会从运行队列中取出G, 然后运行G, 如果G运行完毕或者进入休眠状态, 则从运行队列中取出下一个G运行, 周而复始.
  有时候G需要调用一些无法避免阻塞的原生代码, 这时M会释放持有的P并进入阻塞状态, 其他M会取得这个P并继续运行队列中的G.
  go需要保证有足够的M可以运行G, 不让CPU闲着, 也需要保证M的数量不能过多.

P (process)
  P是process的头文字, 代表M运行G所需要的资源.
  一些讲解协程的文章把P理解为cpu核心, 其实这是错误的.
  虽然P的数量默认等于cpu核心数, 但可以通过环境变量GOMAXPROC修改, 在实际运行时P跟cpu核心并无任何关联.
  P也可以理解为控制go代码的并行度的机制,
  如果P的数量等于1, 代表当前最多只能有一个线程(M)执行go代码,
  如果P的数量等于2, 代表当前最多只能有两个线程(M)执行go代码.
  执行原生代码的线程数量不受P控制.
  因为同一时间只有一个线程(M)可以拥有P, P中的数据都是锁自由(lock free)的, 读写这些数据的效率会非常的高.

数据结构
  在讲解协程的工作流程之前, 还需要理解一些内部的数据结构.

G的状态


  • 空闲中(_Gidle): 表示G刚刚新建, 仍未初始化
  • 待运行(_Grunnable): 表示G在运行队列中, 等待M取出并运行
  • 运行中(_Grunning): 表示M正在运行这个G, 这时候M会拥有一个P
  • 系统调用中(_Gsyscall): 表示M正在运行这个G发起的系统调用, 这时候M并不拥有P
  • 等待中(_Gwaiting): 表示G在等待某些条件完成, 这时候G不在运行也不在运行队列中(可能在channel的等待队列中)
  • 已中止(_Gdead): 表示G未被使用, 可能已执行完毕(并在freelist中等待下次复用)
  • 栈复制中(_Gcopystack): 表示G正在获取一个新的栈空间并把原来的内容复制过去(用于防止GC扫描)
M的状态
  M并没有像G和P一样的状态标记, 但可以认为一个M有以下的状态:


  • 自旋中(spinning): M正在从运行队列获取G, 这时候M会拥有一个P
  • 执行go代码中: M正在执行go代码, 这时候M会拥有一个P
  • 执行原生代码中: M正在执行原生代码或者阻塞的syscall, 这时M并不拥有P
  • 休眠中: M发现无待运行的G时会进入休眠, 并添加到空闲M链表中, 这时M并不拥有P
  自旋中(spinning)这个状态非常重要, 是否需要唤醒或者创建新的M取决于当前自旋中的M的数量.

P的状态


  • 空闲中(_Pidle): 当M发现无待运行的G时会进入休眠, 这时M拥有的P会变为空闲并加到空闲P链表中
  • 运行中(_Prunning): 当M拥有了一个P后, 这个P的状态就会变为运行中, M运行G会使用这个P中的资源
  • 系统调用中(_Psyscall): 当go调用原生代码, 原生代码又反过来调用go代码时, 使用的P会变为此状态
  • GC停止中(_Pgcstop): 当gc停止了整个世界(STW)时, P会变为此状态
  • 已中止(_Pdead): 当P的数量在运行时改变, 且数量减少时多余的P会变为此状态
本地运行队列
  在go中有多个运行队列可以保存待运行(_Grunnable)的G, 它们分别是各个P中的本地运行队列和全局运行队列.
  入队待运行的G时会优先加到当前P的本地运行队列, M获取待运行的G时也会优先从拥有的P的本地运行队列获取,
  本地运行队列入队和出队不需要使用线程锁.
  本地运行队列有数量限制, 当数量达到256个时会入队到全局运行队列.
  本地运行队列的数据结构是环形队列, 由一个256长度的数组和两个序号(head, tail)组成.
  当M从P的本地运行队列获取G时, 如果发现本地队列为空会尝试从其他P盗取一半的G过来,
  这个机制叫做Work Stealing, 详见后面的代码分析.

全局运行队列
  全局运行队列保存在全局变量sched中, 全局运行队列入队和出队需要使用线程锁.
  全局运行队列的数据结构是链表, 由两个指针(head, tail)组成.

空闲M链表
  当M发现无待运行的G时会进入休眠, 并添加到空闲M链表中, 空闲M链表保存在全局变量sched.
  进入休眠的M会等待一个信号量(m.park), 唤醒休眠的M会使用这个信号量.
  go需要保证有足够的M可以运行G, 是通过这样的机制实现的:


  • 入队待运行的G后, 如果当前无自旋的M但是有空闲的P, 就唤醒或者新建一个M
  • 当M离开自旋状态并准备运行出队的G时, 如果当前无自旋的M但是有空闲的P, 就唤醒或者新建一个M
  • 当M离开自旋状态并准备休眠时, 会在离开自旋状态后再次检查所有运行队列, 如果有待运行的G则重新进入自旋状态
  因为"入队待运行的G"和"M离开自旋状态"会同时进行, go会使用这样的检查顺序:
  入队待运行的G => 内存屏障 => 检查当前自旋的M数量 => 唤醒或者新建一个M
  减少当前自旋的M数量 => 内存屏障 => 检查所有运行队列是否有待运行的G => 休眠
  这样可以保证不会出现待运行的G入队了, 也有空闲的资源P, 但无M去执行的情况.

空闲P链表
  当P的本地运行队列中的所有G都运行完毕, 又不能从其他地方拿到G时,
  拥有P的M会释放P并进入休眠状态, 释放的P会变为空闲状态并加到空闲P链表中, 空闲P链表保存在全局变量sched
  下次待运行的G入队时如果发现有空闲的P, 但是又没有自旋中的M时会唤醒或者新建一个M, M会拥有这个P, P会重新变为运行中的状态.

工作流程(概览)
  下图是协程可能出现的工作状态, 图中有4个P, 其中M1~M3正在运行G并且运行后会从拥有的P的运行队列继续获取G:
DSC0000.png

  只看这张图可能有点难以想象实际的工作流程, 这里我根据实际的代码再讲解一遍:
  

package main  

  
import (
  "fmt"
  "time"
  
)
  

  
func printNumber(from, to int, c chan int) {
  for x := from; x

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-598885-1-1.html 上篇帖子: SEO是件贼有意思的事情 golang入坑系列 下篇帖子: Golang之继承,多重继承(struct)
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表