45

Go调度器介绍和容易忽视的问题 - CodeWithTxT - 博客园

 4 years ago
source link: https://www.cnblogs.com/CodeWithTxT/p/11370215.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

16c9f75405d500c0?w=800&h=444&f=jpeg&s=53564

什么是调度器

为了方便刚接触操作系统和高级语言的同学,先用大白话介绍下什么是调度器。
调度,是将多个程序合理的安排到有限的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模型:

16c9f75414987472?w=696&h=454&f=png&s=45664

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模型,如下图:

16c9f75405b40fa9?w=1224&h=1142&f=png&s=85568

其中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
	}

}

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

16c9f75405d500c0?w=800&h=444&f=jpeg&s=53564

实际的数据结果是:

---> 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执行顺序讨论


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK