我是平也,这有一个专注Gopher技术成长的开源项目「go home」

导读

相信很多人都听说过Go语言天然支持高并发,原因是内部有协程(goroutine)加持,可以在一个进程中启动成千上万个协程。那么,它凭什么做到如此高的并发呢?那就需要先了解什么是并发模型。

并发模型

著名的C++专家Herb Sutter曾经说过“免费的午餐已经终结”。为了让代码运行的更快,单纯依靠更快的硬件已经无法得到满足,我们需要利用多核来挖掘并行的价值,而并发模型的目的就是来告诉你不同执行实体之间是如何协作的。

当然,不同的并发模型的协作方式也不尽相同,常见的并发模型有七种:

  • 线程与锁
  • 函数式编程
  • Clojure之道
  • actor
  • 通讯顺序进程(CSP)
  • 数据级并行
  • Lambda架构

而今天,我们只讲与Go语言相关的并发模型CSP,感兴趣的同学可以自行查阅书籍《七周七并发模型》。

CSP篇

CSP,全称Communicating Sequential Processes,意为通讯顺序进程,它是七大并发模型中的一种,它的核心观念是将两个并发执行的实体通过通道channel连接起来,所有的消息都通过channel传输。其实CSP概念早在1978年就被东尼·霍尔提出,由于近来Go语言的兴起,CSP又火了起来。

那么CSP与Go语言有什么关系呢?接下来我们来看Go语言对CSP并发模型的实现——GPM调度模型。

GPM调度模型

GPM代表了三个角色,分别是Goroutine、Processor、Machine。

  • Goroutine:就是咱们常用的用go关键字创建的执行体,它对应一个结构体g,结构体里保存了goroutine的堆栈信息
  • Machine:表示操作系统的线程
  • Processor:表示处理器,有了它才能建立G、M的联系

Goroutine

Goroutine就是代码中使用go关键词创建的执行单元,也是大家熟知的有“轻量级线程”之称的协程,协程是不为操作系统所知的,它由编程语言层面实现,上下文切换不需要经过内核态,再加上协程占用的内存空间极小,所以有着非常大的发展潜力。

go func() {}()

在Go语言中,Goroutine由一个名为runtime.go的结构体表示,该结构体非常复杂,有40多个成员变量,主要存储执行栈、状态、当前占用的线程、调度相关的数据。还有玩大家很想获取的goroutine标识,但是很抱歉,官方考虑到Go语言的发展,设置成私有了,不给你调用。

type g struct {
stack struct {
lo uintptr
hi uintptr
} // 栈内存:[stack.lo, stack.hi)
stackguard0 uintptr
stackguard1 uintptr _panic *_panic
_defer *_defer
m *m // 当前的 m
sched gobuf
stktopsp uintptr // 期望 sp 位于栈顶,用于回溯检查
param unsafe.Pointer // wakeup 唤醒时候传递的参数
atomicstatus uint32
goid int64
preempt bool // 抢占信号,stackguard0 = stackpreempt 的副本
timer *timer // 为 time.Sleep 缓存的计时器 ...
}

Goroutine调度相关的数据存储在sched,在协程切换、恢复上下文的时候用到。

type gobuf struct {
sp uintptr
pc uintptr
g guintptr
ret sys.Uintreg
...
}

Machine

M就是对应操作系统的线程,最多会有GOMAXPROCS个活跃线程能够正常运行,默认情况下GOMAXPROCS被设置为内核数,假如有四个内核,那么默认就创建四个线程,每一个线程对应一个runtime.m结构体。线程数等于CPU个数的原因是,每个线程分配到一个CPU上就不至于出现线程的上下文切换,可以保证系统开销降到最低。

type m struct {
g0 *g
curg *g
...
}

M里面存了两个比较重要的东西,一个是g0,一个是curg。

  • g0:会深度参与运行时的调度过程,比如goroutine的创建、内存分配等
  • curg:代表当前正在线程上执行的goroutine。

刚才说P是负责M与G的关联,所以M里面还要存储与P相关的数据。

type m struct {
...
p puintptr
nextp puintptr
oldp puintptr
}
  • p:正在运行代码的处理器
  • nextp:暂存的处理器
  • old:系统调用之前的线程的处理器

Processor

Proccessor负责Machine与Goroutine的连接,它能提供线程需要的上下文环境,也能分配G到它应该去的线程上执行,有了它,每个G都能得到合理的调用,每个线程都不再浑水摸鱼,真是居家必备之良品。

同样的,处理器的数量也是默认按照GOMAXPROCS来设置的,与线程的数量一一对应。

type p struct {
m muintptr runqhead uint32
runqtail uint32
runq [256]guintptr
runnext guintptr
...
}

结构体P中存储了性能追踪、垃圾回收、计时器等相关的字段外,还存储了处理器的待运行队列,队列中存储的是待执行的Goroutine列表。

三者的关系

首先,默认启动四个线程四个处理器,然后互相绑定。

这个时候,一个Goroutine结构体被创建,在进行函数体地址、参数起始地址、参数长度等信息以及调度相关属性更新之后,它就要进到一个处理器的队列等待发车。

啥,又创建了一个G?那就轮流往其他P里面放呗,相信你排队取号的时候看到其他窗口没人排队也会过去的。

假如有很多G,都塞满了怎么办呢?那就不把G塞到处理器的私有队列里了,而是把它塞到全局队列里(候车大厅)。

除了往里塞之外,M这边还要疯狂往外取,首先去处理器的私有队列里取G执行,如果取完的话就去全局队列取,如果全局队列里也没有的话,就去其他处理器队列里偷,哇,这么饥渴,简直是恶魔啊!

如果哪里都没找到要执行的G呢?那M就会因为太失望和P断开关系,然后去睡觉(idle)了。

那如果两个Goroutine正在通过channel做一些恩恩爱爱的事阻塞住了怎么办,难道M要等他们完事了再继续执行?显然不会,M并不稀罕这对Go男女,而会转身去找别的G执行。

系统调用

如果G进行了系统调用syscall,M也会跟着进入系统调用状态,那么这个P留在这里就浪费了,怎么办呢?这点精妙之处在于,P不会傻傻的等待G和M系统调用完成,而会去找其他比较闲的M执行其他的G。

当G完成了系统调用,因为要继续往下执行,所以必须要再找一个空闲的处理器发车。

如果没有空闲的处理器了,那就只能把G放回全局队列当中等待分配。

sysmon

sysmon是我们的保洁阿姨,它是一个M,又叫监控线程,不需要P就可以独立运行,每20us~10ms会被唤醒一次出来打扫卫生,主要工作就是回收垃圾、回收长时间系统调度阻塞的P、向长时间运行的G发出抢占调度等等。

词条解释

东尼·霍尔

东尼·霍尔,英国计算机科学家,图灵奖得主,他设计了牛气冲天的快速排序算法、霍尔逻辑以及CSP模型。2011年获颁约翰·冯诺依曼奖。


感谢大家的观看,如果觉得文章对你有所帮助,欢迎关注公众号「平也」,聚焦Go语言与技术原理。

Go语言的GPM调度器是什么?的更多相关文章

  1. Go调度器介绍和容易忽视的问题

    本文记录了本人对Golang调度器的理解和跟踪调度器的方法,特别是一个容易忽略的goroutine执行顺序问题,看了很多篇Golang调度器的文章都没提到这个点,分享出来一起学习,欢迎交流指正. 什么 ...

  2. [翻译]Go语言调度器

    Go语言调度器 译序 本文翻译 Daniel Morsing 的博文 The Go scheduler.个人认为这篇文章把Go Routine和调度器的知识讲的浅显易懂.作为一篇介绍性的文章.非常不错 ...

  3. go语言调度器源代码情景分析之六:go汇编语言

    go语言runtime(包括调度器)源代码中有部分代码是用汇编语言编写的,不过这些汇编代码并非针对特定体系结构的汇编代码,而是go语言引入的一种伪汇编,它同样也需要经过汇编器转换成机器指令才能被CPU ...

  4. go语言调度器源代码情景分析之五:汇编指令

    本文是<go调度器源代码情景分析>系列 第一章 预备知识的第4小节. 汇编语言是每位后端程序员都应该掌握的一门语言,因为学会了汇编语言,不管是对我们调试程序还是研究与理解计算机底层的一些运 ...

  5. go语言调度器源代码情景分析之四:函数调用栈

    本文是<go调度器源代码情景分析>系列 第一章 预备知识的第3小节. 什么是栈 栈是一种“后进先出”的数据结构,它相当于一个容器,当需要往容器里面添加元素时只能放在最上面的一个元素之上,需 ...

  6. go语言调度器源代码情景分析之三:内存

    本文是<go调度器源代码情景分析>系列 第一章 预备知识的第2小节. 内存是计算机系统的存储设备,其主要作用是协助CPU在执行程序时存储数据和指令. 内存由大量内存单元组成,内存单元大小为 ...

  7. go语言调度器源代码情景分析之二:CPU寄存器

    本文是<go调度器源代码情景分析>系列 第一章 预备知识的第1小节. 寄存器是CPU内部的存储单元,用于存放从内存读取而来的数据(包括指令)和CPU运算的中间结果,之所以要使用寄存器来临时 ...

  8. go语言调度器源代码情景分析之一:开篇语

    专题简介 本专题以精心设计的情景为线索,结合go语言最新1.12版源代码深入细致的分析了goroutine调度器实现原理. 适宜读者 go语言开发人员 对线程调度器工作原理感兴趣的工程师 对计算机底层 ...

  9. Go语言goroutine调度器初始化(12)

    本文是<Go语言调度器源代码情景分析>系列的第12篇,也是第二章的第2小节. 本章将以下面这个简单的Hello World程序为例,通过跟踪其从启动到退出这一完整的运行流程来分析Go语言调 ...

随机推荐

  1. MySQL InnoDB表的碎片量化和整理(data free能否用来衡量碎片?)

    网络上有很多MySQL表碎片整理的问题,大多数是通过demo一个表然后参考data free来进行碎片整理,这种方式对myisam引擎或者其他引擎可能有效(本人没有做详细的测试).对Innodb引擎是 ...

  2. Http报文和Request和Response的常用方法

    简述 它是HTTP应用程序之间发送的数据块.这些数据块以一些文本形式的元信息开头,这些信息描述了报文的内容及含义,后面跟着可选的数据部分.这些报文都是在客户端.服务器和代理之间流动. HTTP报文的流 ...

  3. vue 拖拽框架 draggable

    直接使用  npm i -S vuedraggable  页面使用 <template> <div class="addProduct"> <el-d ...

  4. 动态网站项目(Dynamic Web Project)CRUD(增删改查)功能的实现(mvc(五层架构)+jdbc+servlet+tomcat7.0+jdk1.8),前端使用JSP+JSTL+EL组合

    代码分享链接 https://pan.baidu.com/s/1UM0grvpttHW9idisiqa6rA    提取码:hx7c 图示           项目结构 1.SelectAllUser ...

  5. 99%的程序员都在用Lombok,原理竟然这么简单?我也手撸了一个!|建议收藏!!!

    罗曼罗兰说过:世界上只有一种英雄主义,就是看清生活的真相之后依然热爱生活. 对于 Lombok 我相信大部分人都不陌生,但对于它的实现原理以及缺点却鲜为人知,而本文将会从 Lombok 的原理出发,手 ...

  6. Nature重磅:华裔科学家成功解码脑电波,AI直接从大脑中合成语音

    [导读]Nature发表华裔作者论文:通过解码大脑活动提升语音的清晰度,使用深度学习方法直接从大脑信号中产生口语句子,达到150个单词,接近正常人水平. 大脑活动能够解码成语音了. 说话似乎是一项毫不 ...

  7. mysql之日志

    我是李福春,我在准备面试,今天的题目是: mysql的redolog和binlog有什么区别? 答: 如下面的表格, redolog vs binlog 然后我们扩展一下,因为日志主要是记录的修改日志 ...

  8. 区间dp暂时的理解

    因为刚刚看了区间dp,所以写一下对区间dp的理解. 例题: 石子归并 51Nod - 1021 看了一篇博客,觉得他说得比较容易理解,所以再次重复一遍: 假如你是上帝,已经知道了1~n堆石子的最优解, ...

  9. Secret Milking Machine POJ - 2455 网络流(Dinic算法---广搜判断+深搜增广)+时间优化+二分

    题意: 第一行输入N M C ,表示从1到N有M条无向边,现在要从1走到N 走C次完全不同的路径,求最长边的最小值.下面M行是从a点到b点的距离. 建图: 题上说从两点之间可以有多条边,问的是从1~N ...

  10. python学习第二节 数据类型、字符编码、文件处理

    标准数据类型 Python3 中有六个标准的数据类型: Number(数字) String(字符串) List(列表) Tuple(元组) Sets(集合) Dictionary(字典) 数字 #整型 ...