Golang 协程与调度器原理
思考从容器上该如何设置 GOMAXPROCS 大小引发,这个数字设置多少合理,其到底限制了什么,cpu 核数,系统线程数还是协程数?背景 Go 语言可以说为并发而生。Go 从语言级别支持并发,通过轻量级协程 Goroutine 来实现程序并发运行,
go
关键字的强大与简洁是其它语言不可及的,接下来让我们一起来探索 Golang 中 Goroutine 与协程调度器设计的一些原理吧。Go 协程 概念
进程: 操作系统分配系统资源(cpu 时间片,内存等)的最小单位
线程:轻量级进程,是操作系统调度的最小单位
协程:轻量级线程,协程的调度由程序控制
怎么理解
进程,线程的两个最小单位如何理解? 在早期面向进程设计的计算机结构中,进程就是操作系统分配系统资源与操作系统调度的最小单位
但在现代的计算结构中,进程升级为线程的容器,多个线程共享一个进程内的系统资源,cpu 执行(调度)对象是线程
轻量级进程与轻量级线程如何理解 轻量级进程:如下图各个进程拥有独立的虚拟内存空间,里面的资源包括 栈,代码,数据,堆... 而线程拥有独立的栈,但是共享进程全部的资源。在 linux 的实现中,进程与线程的底层数据结构是一致的,只是同一进程下线程会共享资源。
文章图片
轻量级线程:线程栈大小固定 8 M,协程栈:2 KB ,动态增长。一对线程里对应多个协程,可以减少线程的数量
文章图片
协程相对线程有什么优势
- 轻量级 (MB vs KB)
- 切换代价低 (调度由程序控制,不需要进入内核空间。需要保存上下文一般少一些)
- 切换频率低,协程协作式调度,线程调度由操作系统控制,需要保证公平性,当线程数量一多,切换频率相对比较高
GM 模型
goroutine 的调度其实是一个生产者-消费者模型。
生产者:程序起 goroutine(G)
消费者:系统线程(M)去消费(执行)goroutine
自然的,在生产者与消费者中间还需要有一个队列来暂存没有消费过的 goroutine。在
Go 1.1
版本,用的就是这种模型。文章图片
GM 模型问题
- M 是并发的,每次访问这个全局队列需要全局锁,锁竞争比较严重
- 忽略了 G 之间的关系,例如,M1 执行 G1 时,G1 创建了 G2,为了执行 G2 很有可能放到 M2 中执行。而 G1,G2 是相关的,缓存大概率是比较接近的,这样会导致性能下降
- 每一个 M 都分配一块缓存 MCache,比较浪费
GMP 模型
- G:go 协程
- M:系统线程
- P:逻辑处理器,负责提供相关的上下文环境,内存缓存的管理, Goroutine任务队列等
文章图片
P 结构特点
- M 通过 P 取 G 时,并发访问大大降低,本地队列不需要全局锁了。
- 每个 P 的本地 G 队列长度限定在 256,而 goroutine 的数量是不定的,因此 Go 还保留了一个无限长度的全局队列。
- 本地队列数据结构是数组,全局队列数据结构是链表
- P 中除了本地队列,还加了一个 runnext 的结构,为了优先执行刚创建的 goroutine
- MCache 从 M 移到了P
- 通过设置 GOMAXPROCS 控制 P 的数量
- 先从绑定的 P 本地队列(优先 runnext)获取 G
- 定期从全局队列中获取 G:每执行 61 次调度会看一下全部队列(保证公平),并且在这个过程会把全局队列中 G 分给各个 P
- work stealing,若全局队列没有 G,则随机选择一个 P 偷一半的任务过来,若没有任务可偷,线程休眠。偷任务会导致并发访问本地队列,因此操作本地队列需要加自旋锁
文章图片
G 的生产逻辑
- 使用
go func
, 产生了一个 G 结构体,会优先选择放在当前 P 的 runnext - 若 runnext 满了,把当前 runnext 里的 goroutine 踢出,放在本地队列尾,再把当前 gorouine 放入 runnext
- 若本地队列也满了,把本地队列的一半的 G 与 被踢出 runnext 的 G,放到全局队列中
- 全局锁,P 中的协程是串行的
- 数据局部性,G 创建时优先在 P 的本地队列,M 获取可用 P 时,优先之前绑定的 P
- 内存消耗问题,多个线程共用 MCache
参考
- https://docs.google.com/docum...
- https://stackoverflow.com/que...
- https://cloud.tencent.com/dev...
- https://yizhi.ren/2019/06/03/...
- https://segmentfault.com/a/11...
- https://www.zhihu.com/questio...
- https://www.bookstack.cn/read...
推荐阅读
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- Docker应用:容器间通信与Mariadb数据库主从复制
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量
- 第326天
- Shell-Bash变量与运算符
- 逻辑回归的理解与python示例
- Guava|Guava RateLimiter与限流算法
- 我和你之前距离
- CGI,FastCGI,PHP-CGI与PHP-FPM
- 原生家庭之痛与超越