Go 原理之 GMP 并发调度模型

一、Go 的协程 goroutine

go 的特性:协程(goroutine),goroutine 是 go 自己实现的、为了解决线程的性能问题,goroutine 协程是用户态的,由 go runtime 创建和销毁,没有内核消耗,线程是内核态的,与操作系统相关,创建和销毁成本较高。

goroutine 提高 cpu 的利用率,解决了高消耗的CPU调度,用户态的轻量级的线程,约4k

这也是 go 为什么性能那么好的原因,而 go 实现 goroutine 协程的原理:GMP 调度模型

二、GMP 调度模型

  • G:

    goroutine 协程,go runtime 包自己实现的一种数据结构,存储执行时唯一的内存栈信息

  • P:

    processor 处理器,go runtime 包实现的调度器,主要用来并发调度 goroutine 协程的启动、执行、等待、暂停、销毁等生命周期

  • M:

    thread 线程,就是我们平时理解的线程,如果你不理解什么是线程,请参考文章 进程线程协程的概念和区别

那么 go 的协程 goroutine 是如何实现以及调度执行的过程是什么样子的呢?

比如我们使用 go 开启一个协程:

go 复制代码
go func(){
    // 开启协程,处理逻辑
}()

'go func()'经历了哪些过程

    1. 通过 'go func()' 创建一个 goroutine(数据结构,有个唯一 gid,以及内存栈信息),这里称为:G
    1. 有两个存储 G 的队列(本地P队列,全局队列),新创建的 G 会优先加入本地 P 队列中,如果满了就会保存在全局队列中
    1. G 最终会通过 P 调度运行在 M 中,MP 是组合(一个M必须持有一个P,M:P=1:1)
    1. M 会从 P 的队列中弹出一个可执行的 G 来执行,如果没有,则会从全局队列中获取,

      全局队列也没有,则会从其他 MP 队列中偷取一个 G执行,从其他 P 偷的方式称为 work stealing

    1. 一个 M 调度 G 执行是一个循环过程
    1. 当 M 执行 G 过程中发生 systemCall 阻塞,M 会阻塞,如果当前有一些 G 在执行,runtime 会把这个线程 M 从 P 中摘除(detach),此时 P 会和 M 解绑即 hand off,然后再创建/从休眠队列中取一个 M 来服务这个 P
    • 系统调用(如文件IO)阻塞(同步):阻塞MG,M与P解绑

    • 网络 IO 调用阻塞(异步):G 移动到NetPoller,M 继续执行 P 中的 G

    • mutex/chan阻塞(异步):G 移动到 chan 的等待队列中,M 继续执行 P 中的 G

    1. 当 M 系统调用结束后进入休眠/销毁状态,这个 G 会尝试获取一个空闲的 P 执行,如果没有,这个 G 会放入全局队列

M 每隔约 10ms 会切换一个 G,被切换的 G 会重新回到本地P队列

如果在 Goroutine 去执行一个 sleep 操作,导致 M 被阻塞了。Go 程序后台有一个监控线程 sysmon,它监控那些长时间运行的 G 任务然后设置可以强占的标识符,别的 Goroutine 就可以抢先进来执行。

三、M0 & G0 的启动

go 启动的时候,默认会启动 M0 线程 和 G0 协程

M0:编号为 0 的主线程

GO:编号为 0 的主协程

四、 协程 goroutine 的调度策略

  • 队列轮转:

    P 会周期性的将G调度到M中执行,执行一段时间后,保存上下文,将 G 放到队列尾部,然后从队列中再取出一个G进行调度

    除此之外,P还会周期性的查看全局队列是否有G等待调度到M中执行

  • 系统调度:

    当 G0 即将进入系统调用时,M0 将释放 P,进而某个空闲的 M1 获取 P,继续执行 P 队列中剩下的 G。

    M1 的来源有可能是 M 的缓存池,也可能是新建的

    当 G0 系统调用结束后,如果有空闲的P,则获取一个P,继续执行 G0。如果没有,则将 G0 放入全局队列,等待被其他的 P 调度。然后 M0 将进入缓存池睡眠

  • 抢占式调度:

    sysmon 监控协程,如果 g 运行时间过长 10 ms,那会发送信号给到 m,g 会被挂起,m继续执行 p 中的 g,防止其他 g 被饿死

五、协程的生命周期

创建、等待(调用 gopark 进入等待状态)、唤醒执行(调用 goready 唤醒等待的 g 执行)、销毁

五、GMP 的数量

G 的初始化大小是 2-4 k,具体数量由内存决定,

P 的数量由用户设置的 GoMAXPROCS 决定,等于CPU的核心数,但是不论 GoMAXPROCS 设置为多大,P 的储存G的数量最大为 256

M 默认限制 10000

常见问题

1. Golang 为什么要创建 goroutine 协程

轻量:1.大小只有 2-4 k,用户级线程,减少了内核态切换创建的开销

操作系统中虽然已经有了多线程、多进程来解决高并发的问题,但是在当今互联网海量高并发场景下,对性能的要求也越来越苛刻,大量的进程/线程会出现内存占用高、CPU消耗多的问题,很多服务的改造与重构也是为了降本增效。

一个进程可以关联多个线程,线程之间会共享进程的一些资源,比如内存地址空间、打开的文件、进程基础信息等,每个线程也都会有自己的栈以及寄存器信息等,线程相比进程更加轻量,而协程相对线程更加轻量,多个协程会关联到一个线程,协程之间会共享线程的一些信息,每个协程也会有自己的栈空间,所以也会更加轻量级。从进程到线程再到协程,其实是一个不断共享,减少切换成本的过程。

Golang 使用协程主要有以下几个原因:

● 大小

协程大概是2-4k,线程大概是1m

● 创建、切换和销毁

协程是用户态的,由 runtime 创建和销毁,没有内核消耗,线程是内核态的,与操作系统相关,创建和销毁成本较高

2. 什么是 CSP 并发模型?

CSP 并发模型:不要以共享内存的方式来通信,而以通信的方式来共享内存

go 实现 CSP 并发模式是通过: goroutine + chan

3. G 调度执行中断是如何恢复的?

G 是一个数据结构,存储上下文堆栈信息

中断的时候将寄存器里的栈信息,保存到自己的 G 对象(sudog)里面。当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面,这样就接着上次之后运行了。

4. 当 G 阻塞时,g、m、p 会发生什么

G 的状态会从运行态变为阻塞态,放入 P 等待队列

M 会从该 Goroutine 所在的 P 中分离出来,转而执行其他 Goroutine

P 会从该 Goroutine 所在的 M 中分离出来,将该 Goroutine 放入等待队列中,并从空闲的 M 队列中取出一个 M,将其绑定到该 P 上

5. runtime 是什么?

golang 底层的基础设施:

  • GPM 的创建和调度

  • 内存分配

  • GC

  • 内置函数如 map,chan,slice,反射的实现等

  • pprof,trace,CGO

  • 操作系统以及 CPU 的一些封装

  • ....

基本上就是 go 的底层所在了

6. 怎么启动第一个 goroutine?

main 启动函数会默认启动 G0 协程

7. Go 的协程为什么那么好,与线程的区别

● 大小

协程大概是 2k-4k,线程大概是1m

● 创建、切换和销毁

协程是用户态的,由runtime创建和销毁,没有内核消耗,线程是内核态的,与操作系统相关,创建和销毁成本较高

提高cpu的利用率,解决了高消耗的CPU调度,用户态的轻量级的线程,约4k

减少了内核切换成本,操作系统分为用户态和内核态(表示操作系统底层)

8. 线程与协程的区别

一个线程有多个协程,协程是用户态的轻量级的线程,非抢占式的,由用户控制,没有内核切换的开销

原文地址

Go 原理之 GMP 并发调度模型

相关推荐
花酒锄作田4 天前
Gin 框架中的规范响应格式设计与实现
golang·gin
qwfys2004 天前
How to install golang 1.26.0 to Ubuntu 24.04
ubuntu·golang·install
codeejun4 天前
每日一Go-25、Go语言进阶:深入并发模式1
开发语言·后端·golang
石牌桥网管4 天前
Go 泛型(Generics)
服务器·开发语言·golang
小二·4 天前
Go 语言系统编程与云原生开发实战(第21篇)
开发语言·云原生·golang
小二·4 天前
Go 语言系统编程与云原生开发实战(第20篇)
开发语言·云原生·golang
女王大人万岁4 天前
Golang实战Eclipse Paho MQTT库:MQTT通信全解析
服务器·开发语言·后端·golang
codeejun4 天前
每日一Go-24、Go语言实战-综合项目:规划与搭建
开发语言·后端·golang
石牌桥网管4 天前
Go类型断言
开发语言·后端·golang
普通网友5 天前
PHP语言的正则表达式
开发语言·后端·golang