本文记录了本人对Golang调度器的理解和跟踪调度器的方法,特别是一个容易忽略的goroutine执行顺序问题,看了很多篇Golang调度器的文章都没提到这个点,分享出来一起学习,欢迎交流指正。

什么是调度器

为了方便刚接触操作系统和高级语言的同学,先用大白话介绍下什么是调度器。

调度,是将多个程序合理的安排到有限的CPU上来使得每个程序都能够得以执行,实现宏观的并发执行。比如我们的电脑CPU只有四核甚至双核,可是我们却可以在电脑上同时运行几十个程序,这就是操作系统调度器的功劳。但操作系统调度的是进程和线程,线程简单地说就是轻量级的进程,但是每个线程仍需要MB级别的内存,而且如果两个切换的线程在不同的进程中,还需要进程切换,会使CPU在调度这件事上花费大量时间。

为了更合理的利用CPU,Golang通过goroutine原生支持高并发,goroutine是由go调度器在语言层面进行调度,将goroutine安排到线程上,可以更充分地利用CPU。

Golang的调度器

Golang的调度器在runtime中实现,我们每个运行的程序执行前都会运行一个runtime负责调度goroutine,我们写的代码入口要在main包下的main函数中也是因为runtime.main函数会调用main.main。Golang的调度器在2012被重写过一次,现在使用的是新版的G-P-M调度器,但是我们还是先来看下老的G-M调度器,这样才可以更好的体会当前调度器的强大之处。

G-M模型:

下面是旧调度器的G-P模型:



M:代表线程,goroutine都是由线程来执行的;

Global G Queue:全局goroutine队列,其中G就代表goroutine,所有M都从这个队列中取出goroutine来执行。

这种模型比较简单,但是问题也很明显:

  1. 多个M访问一个公共的全局G队列,每次都需要加互斥锁保护,造成激烈的锁竞争和阻塞;
  2. 局部性很差,即如果M1上的G1创建了G2,需要将G2交给M2执行,但G1和G2是相关的,最好放在同一个M上执行。
  3. M中有mcache(内存分配状态),消耗大量内存和较差的局部性。
  4. 系统调用syscall会阻塞线程,浪费不能合理的利用CPU。

G-P-M模型

后来Go语言开发者改善了调度器为G-P-M模型,如下图:

其中G还是代表goroutine,M代表线程,全局队列依然存在;而新增加的P代表逻辑processor,现在G的眼中只有P,在G的眼里P就是它的CPU。并且给每个P新增加了局部队列来保存本P要处理的goroutine。

这个模型的调度方法如下:

  1. 每个P有个局部队列,局部队列保存待执行的goroutine
  2. 每个P和一个M绑定,M是真正的执行P中goroutine的实体
  3. 正常情况下,M从绑定的P中的局部队列获取G来执行
  4. 当M绑定的P的的局部队列已经满了之后就会把goroutine放到全局队列
  5. M是复用的,不需要反复销毁和创建,拥有work stealing和hand off策略保证线程的高效利用。
  6. 当M绑定的P的局部队列为空时,M会从其他P的局部队列中偷取G来执行,即work stealing;当其他P偷取不到G时,M会从全局队列获取到本地队列来执行G。
  7. 当G因系统调用(syscall)阻塞时会阻塞M,此时P会和M解绑即hand off,并寻找新的idle的M,若没有idle的M就会新建一个M。
  8. 当G因channel或者network I/O阻塞时,不会阻塞M,M会寻找其他runnable的G;当阻塞的G恢复后会重新进入runnable进入P队列等待执行
  9. mcache(内存分配状态)位于P,所以G可以跨M调度,不再存在跨M调度局部性差的问题
  10. G是抢占调度。不像操作系统按时间片调度线程那样,Go调度器没有时间片概念,G因阻塞和被抢占而暂停,并且G只能在函数调用时有可能被抢占,极端情况下如果G一直做死循环就会霸占一个P和M,Go调度器也无能为力。

Go调度器奇怪的执行顺序

是不是感觉自己对Go调度器工作原理已经有个初步的了解了?下面指出一个坑给你踩一下,小心了!

请看下面这段代码输出什么:

func main() {

	done := make(chan bool)

	values := []string{"a", "b", "c"}
for _, v := range values {
fmt.Println("--->", v)
go func(u string) {
fmt.Println(u)
done <- true
}(v)
} // wait for all goroutines to complete before exiting
for _ = range values {
<-done
} }

先仔细想一下再看答案哦!

实际的数据结果是:

---> a
---> b
---> c
c
b
a

Go调度器示例代码可以在跟着示例代码学golang中查看,持续更新中,想系统学习Golang的同学可以关注一下。

可能你的第一反应是“不应该是输出a,b,c,吗?为什么输出是c,a,b呢?”

这里我们虽然是使用for循环创建了3个goroutine,而且创建顺序是a,b,c,按之前的分析应该是将a,b,c三个goroutine依次放进P的局部队列,然后按照顺序依次执行a,b,c所在的goroutine,为什么每次都是先执行c所在的goroutine呢?这是因为同一逻辑处理器中三个任务被创建后 理论上会按顺序 被放在同一个任务队列,但实际上最后那个任务会被放在专一的next(下一个要被执行的任务的意思)的位置,所以优先级最高,最可能先被执行,所以表现为在同一个goroutine中创建的多个任务中最后创建那个任务最可能先被执行

这段解释来自参考文章《Goroutine执行顺序讨论》中。

调度器状态的查看方法

GODEBUG这个Go运行时环境变量很是强大,通过给其传入不同的key1=value1,key2=value2… 组合,Go的runtime会输出不同的调试信息,比如在这里我们给GODEBUG传入了”schedtrace=1000″,其含义就是每1000ms,打印输出一次goroutine scheduler的状态。

下面演示使用Golang强大的GODEBUG环境变量可以查看当前程序中Go调度器的状态:

环境为Windows10的Linux子系统(WSL),WSL搭建和使用的代码在learn-golang项目有整理,代码在文末参考的鸟窝的文章中也可以找到。

 func main() {
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go work(&wg)
}
wg.Wait()
// Wait to see the global run queue deplete.
time.Sleep(3 * time.Second)
}
func work(wg *sync.WaitGroup) { time.Sleep(time.Second)
var counter int
for i := 0; i < 1e10; i++ {
counter++
}
wg.Done()
}

编译指令:

go build 01_GODEBUG-schedtrace.go
GODEBUG=schedtrace=1000 ./01_GODEBUG-schedtrace

结果:

SCHED 0ms: gomaxprocs=4 idleprocs=1 threads=5 spinningthreads=1 idlethreads=0 runqueue=0 [4 0 4 0]
SCHED 1000ms: gomaxprocs=4 idleprocs=4 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
SCHED 2007ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 3025ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 4033ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 5048ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 6079ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 7081ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 8092ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 9113ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 10129ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 11134ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 12157ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 13170ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 14183ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 15187ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 16187ms: gomaxprocs=4 idleprocs=2 threads=8 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0]
SCHED 17190ms: gomaxprocs=4 idleprocs=2 threads=8 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0]
SCHED 18193ms: gomaxprocs=4 idleprocs=2 threads=8 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0]
SCHED 19196ms: gomaxprocs=4 idleprocs=2 threads=8 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0]
SCHED 20200ms: gomaxprocs=4 idleprocs=4 threads=8 spinningthreads=0 idlethreads=6 runqueue=0 [0 0 0 0]
SCHED 21210ms: gomaxprocs=4 idleprocs=4 threads=8 spinningthreads=0 idlethreads=6 runqueue=0 [0 0 0 0]
SCHED 22219ms: gomaxprocs=4 idleprocs=4 threads=8 spinningthreads=0 idlethreads=6 runqueue=0 [0 0 0 0]

看到怎么多输出不要慌, 了解每个字段的含义就很清晰了:

  • SCHED 1000ms

    自程序运行开始经历的时间
  • gomaxprocs=4

    当前程序使用的逻辑processor,即P,小于等于CPU的核数。
  • idleprocs=4

    空闲的线程数
  • threads=8

    当前程序的总线程数M,包括在执行G的和空闲的
  • spinningthreads=0

    处于自旋状态的线程,即M在绑定的P的局部队列和全局队列都没有G,M没有销毁而是在四处寻觅有没有可以steal的G,这样可以减少线程的大量创建。
  • idlethreads=3

    处于idle空闲状态的线程
  • runqueue=0

    全局队列中G的数目
  • [0 0 0 6]

    本地队列中的每个P的局部队列中G的数目,我的电脑是四核所有有四个P。

上面的输出信息已经足够我们了解我们的程序运行状况,要想看每个goroutine、m和p的详细调度信息,可以在GODEBUG时加入,scheddetail

 GODEBUG=schedtrace=1000,scheddetail=1 ./01_GODEBUG-schedtrace

结果如下:

SCHED 0ms: gomaxprocs=4 idleprocs=4 threads=7 spinningthreads=0 idlethreads=2 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
P0: status=0 schedtick=7 syscalltick=1 m=-1 runqsize=0 gfreecnt=0
P1: status=0 schedtick=2 syscalltick=1 m=-1 runqsize=0 gfreecnt=0
P2: status=0 schedtick=1 syscalltick=1 m=-1 runqsize=0 gfreecnt=0
P3: status=0 schedtick=1 syscalltick=1 m=-1 runqsize=0 gfreecnt=0
M6: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
M5: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
M4: p=-1 curg=33 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
M3: p=-1 curg=49 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
M2: p=-1 curg=17 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
M1: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=false blocked=false lockedg=-1
M0: p=-1 curg=14 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
G1: status=4(semacquire) m=-1 lockedm=-1
G2: status=4(force gc (idle)) m=-1 lockedm=-1
G3: status=4(GC sweep wait) m=-1 lockedm=-1
G4: status=4(sleep) m=-1 lockedm=-1
G5: status=4(sleep) m=-1 lockedm=-1
G6: status=4(sleep) m=-1 lockedm=-1
G7: status=4(sleep) m=-1 lockedm=-1
G8: status=4(sleep) m=-1 lockedm=-1
G9: status=4(sleep) m=-1 lockedm=-1
G10: status=4(sleep) m=-1 lockedm=-1
G11: status=4(sleep) m=-1 lockedm=-1
G12: status=4(sleep) m=-1 lockedm=-1
G13: status=4(sleep) m=-1 lockedm=-1
G14: status=3() m=0 lockedm=-1
G33: status=3() m=4 lockedm=-1
G17: status=3() m=2 lockedm=-1
G49: status=3() m=3 lockedm=-1

代码可以在跟着示例代码学golang中查看,持续更新中,想系统学习Golang的同学可以关注一下。

参考资料:

大彬Go调度器系列

也谈goroutine调度器

鸟窝 Go调度器跟踪

Go调度器详解

Goroutine执行顺序讨论

Go调度器介绍和容易忽视的问题的更多相关文章

  1. k8s调度器介绍(调度框架版本)

    从一个pod的创建开始 由kubectl解析创建pod的yaml,发送创建pod请求到APIServer. APIServer首先做权限认证,然后检查信息并把数据存储到ETCD里,创建deployme ...

  2. linux调度器源码分析 - 概述(一)

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 引言 调度器作为操作系统的核心部件,具有非常重要的意义,其随着linux内核的更新也不断进行着更新.本系列文章通 ...

  3. 第1节 yarn:14、yarn集群当中的三种调度器

    yarn当中的调度器介绍: 第一种调度器:FIFO Scheduler  (队列调度器) 把应用按提交的顺序排成一个队列,这是一个先进先出队列,在进行资源分配的时候,先给队列中最头上的应用进行分配资源 ...

  4. YARN调度器(Scheduler)详解

    理想情况下,我们应用对Yarn资源的请求应该立刻得到满足,但现实情况资源往往是有限的,特别是在一个很繁忙的集群,一个应用资源的请求经常需要等待一段时间才能的到相应的资源.在Yarn中,负责给应用分配资 ...

  5. java定时调度器解决方案分类及特性介绍

    什么是定时调度器? 我们知道程序的运行要么是由事件触发的,而这种事件的触发源头往往是用户通过ui交互操作层层传递过来的:但是我们知道还有另外一种由机器系统时间触发的程序运行场景.大家想想是否遇到或者听 ...

  6. Linux IO调度器相关算法介绍(转)

    IO调度器(IO Scheduler)是操作系统用来决定块设备上IO操作提交顺序的方法.存在的目的有两个,一是提高IO吞吐量,二是降低IO响应时间.然而IO吞吐量和IO响应时间往往是矛盾的,为了尽量平 ...

  7. IO调度器原理介绍

    IO调度器(IO Scheduler)是操作系统用来决定块设备上IO操作提交顺序的方法.存在的目的有两个,一是提高IO吞吐量,二是降低IO响应时间.然而IO吞吐量和IO响应时间往往是矛盾的,为了尽量平 ...

  8. YARN中FIFO、Capacity以及Fari调度器的详细介绍

    (1)FIFO Scheduler 将所有的Applications放到队列中,先按照作业的优先级高低.再按照到达时间的先后,为每个app分配资源.如果第一个app需要的资源被满足了,如果还剩下了资源 ...

  9. 大数据之Yarn——Capacity调度器概念以及配置

    试想一下,你现在所在的公司有一个hadoop的集群.但是A项目组经常做一些定时的BI报表,B项目组则经常使用一些软件做一些临时需求.那么他们肯定会遇到同时提交任务的场景,这个时候到底如何分配资源满足这 ...

随机推荐

  1. 百度小程序自定义通用toast组件

    百度小程序Toast组件 author: @TiffanysBear 百度小程序自定义通用toast组件 BdToast百度小程序自定义通用组件-github地址 需求 手百小程序的toast仅支持在 ...

  2. 学习4:内容# 1.列表 # 2.元祖 # 3.range

    1.列表 列表 -- list -- 容器 有序,可变,支持索引 列表: 存储数据,支持的数据类型很多 字符串,数字,布尔值,列表,集合,元祖,字典, 定义一个列表 lst = ["dsb& ...

  3. py+selenium 明明定位不到元素,但却不报错或是报错AttributeError: 'list' object has no attribute 'click'【已解决】

    问题:定位不到元素,但却不报错或者出现报错AttributeError: 'list' object has no attribute 'click' 如图  或者  解决方法:   将”driver ...

  4. Learning the Depths of Moving People by Watching Frozen

    基于双目的传统算法 对静止的物体, 在不同的 viewpoints 同一时刻进行拍摄, 根据拍摄到的结果, 使用三角测量算法计算出平面 2D 图像在 3D 图像中的坐标 单目 Ground Truth ...

  5. duilib加消息

    一.加消息 1. public INotifyUI, 2. void Notify(TNotifyUI& msg); 3. Notify实现 4. m_pManager->AddNoti ...

  6. wordpress备份和还原和迁移

    备份用mysqldump -u root -p test person > backup.sql 还原用mysql -u root -p < ./backup.sql 数据库密码修改后怎么 ...

  7. c++小游戏——三国杀

    #include<iostream> #include<time.h> #include<stdio.h> #include <stdlib.h> us ...

  8. 个人永久性免费-Excel催化剂功能第23波-非同一般地批量拆分工作表

    工作薄的合并,许多Excel插件已有提供,Excel催化剂也提供了最佳的解决方案,另外还有工作薄的拆分和工作表的拆分,同样也是各大插件必备功能. 至于工作薄拆分,那是伪需求,Excel催化剂永远只会带 ...

  9. [leetcode] 113. Path Sum II (Medium)

    原题链接 子母题 112 Path Sum 跟112多了一点就是保存路径 依然用dfs,多了两个vector保存路径 Runtime: 16 ms, faster than 16.09% of C++ ...

  10. MVC+EFCore 完整教程18 -- 升级分布视图至 View Component

    之前我们详细介绍过分布视图(partial view),在有一些更加复杂的场景下,.net core为我们提供了更加强大的组件 view  component, 可以认为view component是 ...