首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >为什么Go的协程调度很快?

为什么Go的协程调度很快?

作者头像
才浅Coding攻略
发布2022-12-12 18:05:02
发布2022-12-12 18:05:02
85100
代码可运行
举报
文章被收录于专栏:才浅coding攻略才浅coding攻略
运行总次数:0
代码可运行

在单进程时代,执行流程单一,计算机只能一个任务一个任务去处理,一切程序只能串行执行,进程阻塞会带来CPU时间浪费;在多进程/线程时代,当一个进程阻塞时,切换到另外等候的进程,时间片轮转法保证了等待的进程都能够被运行,但是进程间的调度会占用CPU大部分时间;在高并发场景下,如果为每个任务都去创建线程是不现实的。是否能在线程基础上再做划分呢?我们将一个线程切分为用户线程(co-routine协程)和内核线程(thread线程),将其绑定在一起,CPU只去操作内核线程thread。

既然能够绑定一个,是否多个协程可以绑定在一个或者多个线程上呢?

N:1关系中thread绑定调度器,由协程调度器连接多个协程,弊端是由于协程调度器轮询访问,当有一个协程阻塞,会导致后续协程访问不到;

M:N关系中多个线程通过协程调度器绑定多个协程,那么这种方案的重点在于对协程调度器的优化。

协程与线程的区别之一是,线程由CPU调度是抢占式的,协程由用户态调度是协作式的,一个协程让出CPU后才执行下一个协程。

Goalng中的goroutine与协程co-routine相比除了名称不一样外,在内存方面做了优化,一个goroutine只占几KB,这表示可以存在大量goroutine,而且调度更为灵活(runtime调度)。

在go中goroutine调度器使用了GMP模型。其中G表示goroutine协程、P表示processor处理器,包含了每个goroutine的资源如栈、堆等,如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列;M表示thread内核线程。

这里P的数量最大值为CPU核心数,当然这个值可以通过环境变量$GOMAXPROCS配置,或者在程序中通过runtime.GOMAXPROCS()来设置。

协程调度器复用线程时线程可以调用以下两种机制:

work stealing机制:当本线程无可运行的G时,尝试先从本地的其他线程绑定的P中偷取G,而不是销毁线程;如果从其他P偷不到G时,它可以从全局队列中获取G,对于偷取数量可以偷一半或者一半以上。

hand off机制:当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行,这个空闲线程可以从休眠M队列中取,如果该队列为空则需创建一个线程,来接收P以及P绑定的可运行的G队列。

以main goroutine为例来看一个go func()调度流程:

代码语言:javascript
代码运行次数:0
运行
复制
package main

import "fmt"

func main() {
    fmt.Println("Hello world")
}

1、runtime创建第一个线程M0:M0是启动进程后的编号为0的主线程,这个M对应的实例会在全局变量runtime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G,在之后M0就和其他的M一样了。

2、runtime创建第一个Go协程G0:G0是每次启动一个M都会第一个创建的goroutine,G0仅用于负责调度G,G0不指向任何可执行函数,每个M都会有一个自己的G0。在调度或系统调用时会使用G0的栈空间,全局变量的G0是M0的G0。一般的G0放在本地队列中。

3、关联M0和G0。

4、调度初始化初始化M0、栈、垃圾回收,以及创建和初始化由GOMAXPROCS个P构成的P列表。

5、创建main()中的goroutine,即runtime.main创建goroutine。

6、启动M0,此时M0已经绑定了P,从P的本地队列中获取G,获取到main goroutine。

7、M绑定P。

8、循环判断M通过P是否能够获取到G。

9、获取不到则M进入休眠队列,等待被唤醒后再重新与P绑定。

10、能够获取到G,则M根据G中的栈信息和调度信息设置运行环境。

11、M执行G。

12、G退出,runtime.main执行Defer和Panic处理,或调用runtime.exit退出程序。


通过Debug trace查看GMP信息,运行以下代码为例:

代码语言:javascript
代码运行次数:0
运行
复制
package main

import (
  "fmt"
  "time"
)

func main() {
  for i := 0; i < 5; i++ {
    time.Sleep(time.Second)
    fmt.Println("hello GMP")
  }
}

我们要循环输出5次“hello GMP”,在终端go build之后,执行:

代码语言:javascript
代码运行次数:0
运行
复制
GODEBUG=schedtrace=1000 ./trace2

trace2是可执行文件,1000表示1000毫秒,结果如下:

代码语言:javascript
代码运行次数:0
运行
复制
SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=5 spinningthreads=1 idlethreads=0 runqueue=0 [1 0 0 0 0 0 0 0]
hello GMP
SCHED 1001ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
SCHED 2005ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
SCHED 3012ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
SCHED 4013ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
  • gomaxprocs:P的数量,即CPU核心数
  • idleprocs:处于空闲状态的P的数量,gomaxprocs-idleprocs为正在运行的数量
  • threads:线程数量(包括M0,包括GODEBBUG调试的线程)
  • spinningthreads:处于自旋状态的thread数量。(即M-P-G0自旋线程)
  • idlethreads:处于空闲状态的thread
  • runqueue:全局G队列中G的数量
  • [1 0 0 0 0 0 0 0]:每个P的本地队列中,目前存在G的数量

https://www.kancloud.cn/aceld/golang 刘丹冰 《GOLANG修养之路》

END

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-03-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 才浅coding攻略 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档