Golang 调度器相关

1、Golang调度器设计思想        

调度器的有两大思想

复用线程:协程本身就是运行在一组线程之上,不需要频繁的创建、销毁线程,而是对线程的复用。在调度器中复用线程还有2个体现:1)work stealing,当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程。2)hand off,当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。

利用并行:GOMAXPROCS设置P的数量,当GOMAXPROCS大于1时,就最多有GOMAXPROCS个线程处于运行状态,这些线程可能分布在多个CPU核上同时运行,使得并发利用并行。另外,GOMAXPROCS也限制了并发的程度,比如GOMAXPROCS = 核数/2,则最多利用了一半的CPU核进行并行。

调度器的两小策略

抢占:在coroutine中要等待一个协程主动让出CPU才执行下一个协程,在Go中,一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死,这就是goroutine不同于coroutine的一个地方。

全局G队列:在新的调度器中依然有全局G队列,但功能已经被弱化了,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G。

2、G,P和M

        调度器运行时负责运行goroutine并把它们影射到操作系统的线程上。goroutine比线程还轻量, 启动的时候花费很少。每个goroutine都是由一个G结构来表示,这个结构体的字段用来跟踪此goroutine的栈(stack)和状态,所以你可以认为G = goroutine

        运行时管理着G并把它们映射到Logical Processor(称之为P). P可以看作是一个抽象的资源或者一个上下文,它需要获取以便操作系统线程(称之为M)可以运行G

        通过runtime.GOMAXPROCS (numLogicalProcessors)可以控制多少P可以获取。如果你需要调整这个参数(大部分情况下你无需调整), 只设置一次, 因为它需要 STW gc pause。

我们知道可以通过

runtime.GOMAXPROCS()

来了设定P的值

Go 1.5开始, Go的GOMAXPROCS默认值已经设置为 CPU的核数, 这允许我们的Go程序充分使用机器的每一个CPU,最大程度的提高我们程序的并发性能。

但其实对于IO密集型的场景,我们可以把GOMAXPROCS的值超过CPU核数,在笔者维护的某个服务中,将GOMAXPROCS设为CPU核数的2倍,压测结果表明,吞吐能力大概能提升10%

        本质上,操作系统运行线程,线程运行你的代码。Go的技巧是编译器会在Go运行时的一些地方插入系统调用, (比如通过channel发送值,调用runtime包等),所以Go可以通知调度器执行特定的操作。

3、宏观看调度器

Golang 调度器相关_第1张图片

自顶向下是调度器的4个部分:

  1. 全局队列(Global Queue):存放等待运行的G。
  2. P的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建G'时,G'优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。
  3. P列表:所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS个。
  4. M:线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列一批G放到P的本地队列,或从其他P的本地队列一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。

Goroutine调度器和OS调度器是通过M结合起来的,每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行

 4、调度器生命周期

         Golang 调度器相关_第2张图片

  1. untime创建最初的线程m0和goroutine g0,并把2者关联。
  2. 调度器初始化:初始化m0、栈、垃圾回收,以及创建和初始化由GOMAXPROCS个P构成的P列表。
  3. 示例代码中的main函数是main.mainruntime中也有1个main函数——runtime.main,代码经过编译后,runtime.main会调用main.main,程序启动时会为runtime.main创建goroutine,称它为main goroutine吧,然后把main goroutine加入到P的本地队列。
  4. 启动m0,m0已经绑定了P,会从P的本地队列获取G,获取到main goroutine。
  5. G拥有栈,M根据G中的栈信息和调度信息设置运行环境
  6. M运行G
  7. G退出,再次回到M获取可运行的G,这样重复下去,直到main.main退出,runtime.main执行Defer和Panic处理,或调用runtime.exit退出程序。

        调度器的生命周期几乎占满了一个Go程序的一生,runtime.main的goroutine执行之前都是为调度器做准备工作,runtime.main的goroutine运行,才是调度器的真正开始,直到runtime.main结束而结束。

 5、12种调度场景

Golang 调度器相关_第3张图片

参考:Go调度器系列(3)图解调度原理

你可能感兴趣的:(Golang,内核,多线程,队列,golang)