golang 中的协程 goroutine,深度理解 GMP 模型

GMP模型

一、前言

1.1 进程与线程

在早期单核CPU的场景下,程序以单进程单线程运行,计算机只能一个任务一个任务的执行,这就造成了进程阻塞时导致CPU资源的浪费。

所以后来就出现了线程,把每个任务单独的放到一个线程中,CPU 按照时间片调度并发的执行每一个线程,这样任务看起来就好像同时在执行。

image-20220213153555702

但是按照线程来调度有两个不可修改的缺点:

  • CPU 资源有一部分被用于切换任务时
  • 内存的占用

上下文的切换

CPU 在执行任务的时候很有可能一个任务还没有执行完,时间片就用完了需要执行下一个任务,这时 CPU 会保存现场,待到下次分配时间片时读取现场继续执行任务,保存和读取现场一定会消耗 CPU 的资源,大量的 CPU 资源就被消耗在线程间上下文的切换。

image-20220213141433908

有时做相同的任务,甚至于在单线程下速度要优于多线程,主要就是由于线程切换上下文带来的消耗。

内存的占用

创建一个线程需要一般情况下需要 4mb 的内存,而创建一个进程在一些操作系统下需要 gb 级别的虚拟内存,实际内存也达到了 mb 的级别,所以说线程和进程的创建是非常消耗资源的。

1.2 协程

实际上一个线程基本可以看作两个部分,用户部分和内核部分。

image-20220213143213330

粗略的看,用户部分就是我们自己写的程序,内核部分就是操作系统对内存、磁盘等资源的操作。我们能否在用户部分创建多个任务共用一个内核线程,并通过一个调度器来控制这些任务的执行从而减少上下文的切换?于是协程(co-routine)出现了。

在 go 语言中称协程为 goroutine

image-20220213143426901

协程除了减少上下文的切换还减少了内存的占用,创建一个携程只需要 2kb 的内存空间,这就可以允许用户创建大量的协程共同工作而不会导致内存的大量占用。

那么协程和线程如何对应才比较合适呢,一般是多个协程对应多个线程。

M : 1

考虑 M 个协程对应 1 个线程的情况:

假如 A, B, C 三个协程共用一个线程,调度器按照 A, B, C 的顺序把每个协程分配给线程去执行,在这种情况下如果 B 协程发生了长时间的阻塞,此时协程 C 也会被阻塞。

所以 M : 1 存在一定的问题。

1 : 1

考虑 1 个协程对应 1 个线程的情况:

这种情况下与多线程几乎没有区别

M : N

考虑 M 个协程对应 N 个线程的情况:

这种情况下就可以解决 M : 1 情况下的阻塞问题,比如只需要将阻塞的协程单独运行在一个线程上,其他任务再拿到一个新的线程上运行,当然这就需要一个非常优秀的调度器来对协程进行调度,这样可以大大的提高 CPU 的利用率。

二、GMP模型

2.1 模型简介

GMP其实是一个缩写,分别代表:

  • G:协程
  • M:线程
  • P:调度器
image-20220213151220612

在系统中的布局如下:

image-20220213151458853

全局队列与本地队列

本地队列和全局队列存放的都是待执行的 goroutine。

一个 goroutine 在创建时优先会存放到 P 的本地队列中,一个本地队列最大能存放 256 个 goroutine,当本地队列存不下了后会存放在全局队列中。

调度器 P

P 是在程序开始运行是创建的,调度器的数量默认与内核的数量相等,可以通过 runtime.GOMAXPROCS 来设置,调度器会从本地队列中取 goroutine 并交给内核线程去执行。

内核线程 M

M 指的是操作系统分配到程序的内核线程数,M 的数量是动态的,会随着 M 的阻塞和空闲进行分配和回收或睡眠。

2.2 调度器的调度策略

GMP 模型有如下的调度策略:

  • 复用线程
    • work stealing
    • hand off
  • 利用并行
  • 抢占
  • 全局 G 队列

线程的复用

work stealing

当一个 P 的本地队列中没有 G 时,它会到其它 P 的本地队列中偷取一半的 G 放在本地执行

image-20220213155159777

hand off

当 P 正在执行的 G 发生了阻塞,此时会创建/唤醒一个新的 M 并继续执行其他的 G,原来的 M 睡眠或销毁,如果阻塞的 G 在阻塞结束后需要继续执行会加入到一个本地队列。

image-20220213180743486

利用并行

可以通过 GOMAXPROCS 限定使用 CPU 的个数,从而让剩下的 CPU 执行其他的线程。

抢占

每个 G1 在分配到 CPU 时如果有 G2 在等待运行,当前的 G1 在运行 10ms 后被正在等待的 G2 抢占 CPU 并重新加入到本地队列或全局队列中去。

这样保证了在并发场景下协程的相对公平,让每个 G 都有机会运行不会等待太久。

全局队列

当一个 P 的本地队列为空,他会尝试去其他本地队列获取 G,如果其他本地队列也为空,就回到全局队列获取 G。

2.3 调度器的生命周期

开始前首先要了解一个特殊的线程 M0 和一个特殊的协程 G0

  • M0
    • 启动程序后编号为 0 的主线程
    • 在全局变量 runtime.m0 中,不需要在堆中额外分配
    • 负责执行初始化操作和启动第一个 G
    • 启动第一个 G 后,M0 就与其他的 M 一样了
  • G0
    • 每启动一个 M,都会创建一个 G 称为 G0
    • G0 仅用于调度其它的 G
    • G0 不指向任何可执行的函数
    • 在调度或系统调用时会使用 M 会切换到 G0 ,来调度
    • M0 的 G0 会放在全局空间

当一个 go 程序启动后:

  1. 启动 M0,M0 启动 G0,初始化 P 列表,全局队列
  2. G0 加载 main 函数生成一个 G
  3. M0 对 G0 解绑,M0 绑定到一个 P 并把 main函数生成的 G 加入本地队列
  4. 程序正常运行(按照 2.2 的调度策略)

2.4 可视化查看GMP

package main

import (
	"fmt"
	"os"
	"runtime/trace"
)

func main() {
	// 1. 创建 trace.out 文件
	f, err := os.OpenFile("trace.out", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
	if err != nil {
		return
	}

	// 运行 trace
	err = trace.Start(f)
	if err != nil {
		return
	}

	// 要调试的代码
	fmt.Println("hello gmp")

	// 关闭trace
	trace.Stop()
}

执行上列代码生成一个 trance.out 文件

go tool trace ./trace.out

通过 go 工具包查看信息

image-20220213184525697

其中包括了 GMP 的信息

三、场景

  • 创建新的 G
    • 当一个 G 创建另一个 G时,会优先放在同一个本地队列中
  • G 执行完毕
    • 当 G 执行完毕后,G0 会被分配资源进行一系列初始化操作,然后从本地队列获取 G
  • 连续创建多个 G
    • 如果连续创建多个 G,首先会将本地队列先创建满
    • 如果本地队列满了,创建新的 G 时会先将本地队列中前一半的 G 打乱并和新创建的 G 一起加入全局队列
    • 重复前两个操作
  • 唤醒一个休眠中的 M
    • 当一个 G 创建一个新的 G 时会尝试唤醒一个 M,唤醒后 M 会尝试与一个 P 去绑定
    • M 会分配给 G0 资源尝试获取 G
    • 如果绑定的 P 是空闲的,M 就会开始自旋尝试获取 G,M此时成为自旋线程
  • 偷取G
    • 自旋线程会尝试到全局获取1个 G,没有则到其他本地队列偷取后半部分的 G
  • G 发生阻塞
    • 阻塞的 G 会被分配给当前 M
    • P 会尝试获取一个空闲的 M,如果没有则进入空闲 P 队列等待有空闲的 M
  • G 的阻塞完成
    • 原来的 M 会先尝试获取原来的 P,失败则尝试获取空闲 P 队列中的 P,还没有则把 G 加入全局队列并且自身加入休眠队列
  • 2
    点赞
  • 7
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:数字20 设计师:CSDN官方博客 返回首页
评论

打赏作者

Aurora & Code Is Law

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值