G-P-M 模型概述

每一个OS线程都有一个固定大小的内存块(一般会是2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。这个固定大小的栈同时很大又很小。因为2MB的栈对于一个小小的goroutine来说是很大的内存浪费,而对于一些复杂的任务(如深度嵌套的递归)来说又显得太小。因此,Go语言做了它自己的『线程』。

在Go语言中,每一个goroutine是一个独立的执行单元,相较于每个OS线程固定分配2M内存的模式,goroutine的栈采取了动态扩容方式, 初始时仅为2KB,随着任务执行按需增长,最大可达1GB(64位机器最大是1G,32位机器最大是256M),且完全由golang自己的调度器 Go Scheduler 来调度。此外,GC还会周期性地将不再使用的内存回收,收缩栈空间。 因此,Go程序可以同时并发成千上万个goroutine是得益于它强劲的调度器和高效的内存模型。Go的创造者大概对goroutine的定位就是屠龙刀,因为他们不仅让goroutine作为golang并发编程的最核心组件(开发者的程序都是基于goroutine运行的)而且golang中的许多标准库的实现也到处能见到goroutine的身影,比如net/http这个包,甚至语言本身的组件runtime运行时和GC垃圾回收器都是运行在goroutine上的,作者对goroutine的厚望可见一斑。

任何用户线程最终肯定都是要交由OS线程来执行的,goroutine(称为G)也不例外,但是G并不直接绑定OS线程运行,而是由Goroutine Scheduler中的 P - Logical Processor (逻辑处理器)来作为两者的『中介』,P可以看作是一个抽象的资源或者一个上下文,一个P绑定一个OS线程,在golang的实现里把OS线程抽象成一个数据结构:M,G实际上是由M通过P来进行调度运行的,但是在G的层面来看,P提供了G运行所需的一切资源和环境,因此在G看来P就是运行它的 “CPU”,由 G、P、M 这三种由Go抽象出来的实现,最终形成了Go调度器的基本结构:

  • G: 表示Goroutine,每个Goroutine对应一个G结构体,G存储Goroutine的运行堆栈、状态以及任务函数,可重用。G并非执行体,每个G需要绑定到P才能被调度执行。
  • P: Processor,表示逻辑处理器, 对G来说,P相当于CPU核,G只有绑定到P(在P的local runq中)才能被调度。对M来说,P提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等,P的数量决定了系统内最大可并行的G的数量(前提:物理CPU核数 >= P的数量),P的数量由用户设置的GOMAXPROCS决定,但是不论GOMAXPROCS设置为多大,P的数量最大为256。
  • M: Machine,OS线程抽象,代表着真正执行计算的资源,在绑定有效的P后,进入schedule循环;而schedule循环的机制大致是从Global队列、P的Local队列以及wait队列中获取G,切换到G的执行栈上并执行G的函数,调用goexit做清理工作并回到M,如此反复。M并不保留G状态,这是G可以跨M调度的基础,M的数量是不定的,由Go Runtime调整,为了防止创建过多OS线程导致系统调度不过来,目前默认最大限制为10000个。

关于P,我们需要再絮叨几句,在Go 1.0发布的时候,它的调度器其实G-M模型,也就是没有P的,调度过程全由G和M完成,这个模型暴露出一些问题:

  • 单一全局互斥锁(Sched.Lock)和集中状态存储的存在导致所有goroutine相关操作,比如:创建、重新调度等都要上锁;
  • goroutine传递问题:M经常在M之间传递『可运行』的goroutine,这导致调度延迟增大以及额外的性能损耗;
  • 每个M做内存缓存,导致内存占用过高,数据局部性较差;
  • 由于syscall调用而形成的剧烈的worker thread阻塞和解除阻塞,导致额外的性能损耗。

这些问题实在太扎眼了,导致Go1.0虽然号称原生支持并发,却在并发性能上一直饱受诟病,然后,Go语言委员会中一个核心开发大佬看不下了,亲自下场重新设计和实现了Go调度器(在原有的G-M模型中引入了P)并且实现了一个叫做 work-stealing 的调度算法:

  • 每个P维护一个G的本地队列;
  • 当一个G被创建出来,或者变为可执行状态时,就把他放到P的可执行队列中;
  • 当一个G在M里执行结束后,P会从队列中把该G取出;如果此时P的队列为空,即没有其他G可以执行, M就随机选择另外一个P,从其可执行的G队列中取走一半。

该算法避免了在goroutine调度时使用全局锁。

至此,Go调度器的基本模型确立:

G-P-M 模型调度

Go调度器工作时会维护两种用来保存G的任务队列:一种是一个Global任务队列,一种是每个P维护的Local任务队列。

当通过go关键字创建一个新的goroutine的时候,它会优先被放入P的本地队列。为了运行goroutine,M需要持有(绑定)一个P,接着M会启动一个OS线程,循环从P的本地队列里取出一个goroutine并执行。当然还有上文提及的 work-stealing调度算法:当M执行完了当前P的Local队列里的所有G后,P也不会就这么在那躺尸啥都不干,它会先尝试从Global队列寻找G来执行,如果Global队列为空,它会随机挑选另外一个P,从它的队列里中拿走一半的G到自己的队列中执行。

如果一切正常,调度器会以上述的那种方式顺畅地运行,但这个世界没这么美好,总有意外发生,以下分析goroutine在两种例外情况下的行为。

Go runtime会在下面的goroutine被阻塞的情况下运行另外一个goroutine:

  • blocking syscall (for example opening a file)
  • network input
  • channel operations
  • primitives in the sync package

这四种场景又可归类为两种类型:

用户态阻塞/唤醒

当goroutine因为channel操作或者network I/O而阻塞时(实际上golang已经用netpoller实现了goroutine网络I/O阻塞不会导致M被阻塞,仅阻塞G,这里仅仅是举个栗子),对应的G会被放置到某个wait队列(如channel的waitq),该G的状态由_Gruning变为_Gwaitting,而M会跳过该G尝试获取并执行下一个G,如果此时没有runnable的G供M运行,那么M将解绑P,并进入sleep状态;当阻塞的G被另一端的G2唤醒时(比如channel的可读/写通知),G被标记为runnable,尝试加入G2所在P的runnext,然后再是P的Local队列和Global队列。

系统调用阻塞

当G被阻塞在某个系统调用上时,此时G会阻塞在_Gsyscall状态,M也处于 block on syscall 状态,此时的M可被抢占调度:执行该G的M会与P解绑,而P则尝试与其它idle的M绑定,继续执行其它G。如果没有其它idle的M,但P的Local队列中仍然有G需要执行,则创建一个新的M;当系统调用完成后,G会重新尝试获取一个idle的P进入它的Local队列恢复执行,如果没有idle的P,G会被标记为runnable加入到Global队列。

以上就是从宏观的角度对Goroutine和它的调度器进行的一些概要性的介绍,当然,Go的调度中更复杂的抢占式调度、阻塞调度的更多细节,大家可以自行去找相关资料深入理解,本文只讲到Go调度器的基本调度过程,为后面自己实现一个Goroutine Pool提供理论基础,这里便不再继续深入上述说的那几个调度了,事实上如果要完全讲清楚Go调度器,一篇文章的篇幅也实在是捉襟见肘,所以想了解更多细节的同学可以去看看Go调度器 G-P-M 模型的设计者 Dmitry Vyukov 写的该模型的设计文档《 Go Preemptive Scheduler Design》以及直接去看源码,G-P-M模型的定义放在src/runtime/runtime2.go里面,而调度过程则放在了src/runtime/proc.go里。

REFERENCE:

Goroutine并发调度模型深度解析&手撸一个协程池

G-P-M 模型的更多相关文章

  1. 点击率模型AUC

    一 背景       首先举个例子:                          正样本(90)                       负样本(10)         模型1预测      ...

  2. ITU-T G.1081 IPTV性能监测点 (Performance monitoring points for IPTV)

    ITU-T 建议书 G.1081 IPTV性能监测点 Performance monitoring points for IPTV Summary Successful deployment of I ...

  3. lvs dr 模型配置详解

    前期准备: 两台服务器 note01(lvs服务器) note02(real sever) 1 首先在note01配置子网卡: ifconfig eth0: :2意思是eth0的子接口,随便一个数字就 ...

  4. 持久化的基于L2正则化和平均滑动模型的MNIST手写数字识别模型

    持久化的基于L2正则化和平均滑动模型的MNIST手写数字识别模型 觉得有用的话,欢迎一起讨论相互学习~Follow Me 参考文献Tensorflow实战Google深度学习框架 实验平台: Tens ...

  5. 复杂模型可解释性方法——LIME

    一.模型可解释性     近年来,机器学习(深度学习)取得了一系列骄人战绩,但是其模型的深度和复杂度远远超出了人类理解的范畴,或者称之为黑盒(机器是否同样不能理解?),当一个机器学习模型泛化性能很好时 ...

  6. Adaboost\GBDT\GBRT\组合算法

    Adaboost\GBDT\GBRT\组合算法(龙心尘老师上课笔记) 一.Bagging (并行bootstrap)& Boosting(串行) 随机森林实际上是bagging的思路,而GBD ...

  7. 0.读书笔记之The major advancements in Deep Learning in 2016

    The major advancements in Deep Learning in 2016 地址:https://tryolabs.com/blog/2016/12/06/major-advanc ...

  8. 机器学习&数据挖掘笔记_24(PGM练习八:结构学习)

    前言: 本次实验包含了2部分:贝叶斯模型参数的学习以及贝叶斯模型结构的学习,在前面的博文PGM练习七:CRF中参数的学习 中我们已经知道怎样学习马尔科夫模型(CRF)的参数,那个实验采用的是优化方法, ...

  9. 机器学习中的Bias(偏差),Error(误差),和Variance(方差)有什么区别和联系?

    前几天搜狗的一道笔试题,大意是在随机森林上增加一棵树,variance和bias如何变化呢? 参考知乎上的讨论:https://www.zhihu.com/question/27068705 另外可参 ...

  10. Python自动化 【第九篇】:Python基础-线程、进程及python GIL全局解释器锁

    本节内容: 进程与线程区别 线程 a)  语法 b)  join c)  线程锁之Lock\Rlock\信号量 d)  将线程变为守护进程 e)  Event事件 f)   queue队列 g)  生 ...

随机推荐

  1. Java学习笔记之---static

    Java学习笔记之---static static不能修饰类,局部变量 (一)静态成员的特征 static+属性  静态属性 无论实例化出来多少个对象,都会共用同一块静态空间,类对象共享 静态成员从第 ...

  2. python爬虫笔记之用cookie访问需要登录的网站

     目标:用cookie访问一个需要登录的网站 如图,直接访问会跳转到登录页面,提示登录. 运行结果: 直接在浏览器上输入该url,网站立马跳转到登录页面.  方法: 1.先手动登录,通过抓包获取coo ...

  3. pytorch实现yolov3(5) 实现端到端的目标检测

    torch实现yolov3(1) torch实现yolov3(2) torch实现yolov3(3) torch实现yolov3(4) 前面4篇已经实现了network的forward,并且将netw ...

  4. 个人永久性免费-Excel催化剂功能第46波-区域集合函数,超乎所求所想

    在常规自定义函数的世界中,一般情况下,仅会输入一堆的参数,最终输出一个结果值,在以往Excel催化剂的自定义函数,已经大量出现输入一堆参数返回多个结果值并自动输出到多个单元格区域内.此项技术可运用的场 ...

  5. 面向对象和pickle模块结合

    面向对象和pickle模块相关 1.面向对象: class 类名: def init(self,参数1,参数2): self.对象的属性1 = 参数1 self.对象的属性2 = 参数2 def 方法 ...

  6. websocket的加密和解密过程

    加密: import struct msg_bytes = "the emperor has not been half-baked in the early days of the col ...

  7. pymysql指南

    1 引言 mysql应该说是如今使用最为普遍的数据库了,没有之一,而Python作为最为流行的语言之一,自然少不了与mysql打交道,pymysql就是使用最多的工具库了. 2 创建库.表 我们先从创 ...

  8. PHP-- B/S结构

    B/S结构(Browser/Server,浏览器/服务器模式),是WEB兴起后的一种网络结构模式,WEB浏览器是客户端最主要的应用软件.这种模式统一了客户端,将系统功能实现的核心部分集中到服务器上,简 ...

  9. 实现一个Golang的reverse函数

    Reverse函数,用来反转列表,本例子用golang实现,反转一个slice列表. 因为slice是引用类型,因此直接修改参数的值即可. func myReverse(l []string) { f ...

  10. chapter01

    1.请用命令查出ifconfig命令程序的绝对路径[root@localhost ~]# which ifconfig 答:/usr/sbin/ifconfig 2.请用命令展示以下命令哪些是内部命令 ...