说实话,网上对于golang调度的文章很多,但是大部分都是从理论上来描述,描写的精确一点的也依然不够直观。今天跟同学在讨论一道题目的时候又好好的理解了下golang的调度。感觉有所收获,记录一下。

问题

在问题开始之前,我们先出一个题目,交替打印数字和字母。

在网上搜索,以及大部分用户自己想,都会写出来类似于以下的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func main() {
	ch1 := make(chan int)
	ch2 := make(chan string)
	str := [5]string{"a", "b", "c", "d", "e"}
	go func() {
		for i := 0; i < 5; i++ {
			ch1 <- i
			fmt.Print(i + 1)
			<-ch2
		}
	}()

	for _, v := range str {
		<-ch1
		fmt.Print(v)
		ch2 <- v
	}
}

看着似乎没有什么问题:main被阻塞,直到协程中写入数据。

协程中写入数据的同时会打印结果,然后阻塞,等main中写入数据。

可是,我们运行这段函数10000次,看看结果

中间偶尔出现了这么几个结果

1
2
3
4
5
6
7
8
1a2b3c4d5e
...
a1b2c3d4e5
1a2b3c4d5e
...
1a2bc34d5e
...
1a2b3cde45

为什么会有这个结果?为什么a还能比1先打印出来?

我们稍后解释,先说一遍原理。

golang调度

网上对于golang调度差不多都是这个图,我们也拿过来借鉴一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
+---------+                                 +---------+
|         |                                 |         |
|   M     |                                 |   M     |
|         |                                 |         |
|         |                                 |         |
+----+----+                                 +----+----+
     ^             本 地 G 队 列                     ^              本 地 G 队 列
     |                                           |
  +--+---+              <---+                 +--+---+              <---+
  |      |       +-----+                      |      |       +-----+
  |  P   +<------+  G  |                      |  P   +<------+  G  |
  |      |       |     |                      |      |       |     |
  +--^---+       +--^--+                      +--^---+       +--^--+
     |              |                            |              |
  +--+--+        +--+--+                      +--+--+        +--+--+
  |  G  |        |  G  |                      |  G  |        |  G  |
  |     |        |     |                      |     |        |     |
  +-----+        +--^--+                      +-----+        +--^--+
                    |                                           |
                    |                                           |
                 +--+---+                                    +--+---+
                 |   G  |                                    |   G  |
                 |      |                                    |      |
                 +------+                                    +------+

G就是我们的go routine, 对应我们代码中的每一个协程;

P就是Processer,是用来调度G,实现M运行G的处理器;

M就是Machine,代表的是物理机的每一个真实的线程。

我们每通过go关键字创建一个协程,就会被放到一个队列里面。然后每当M运行完一个G的时候,P会从本地队列里面取出来一个新的G,然后让M运行。

似乎很简单,可是G是怎么被放给P的?P和M是怎么交互的呢?

创建

我们都知道,直接

1
go do_something()

就可以直接启动一个协程,这个协程是怎么启动的呢?

我们先看下图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
+---------+                                 +---------+
|         |                                 |         |
|   M     |                                 |   M     |
|         |                                 |         |
|         |                                 |         |
+----+----+                                 +----+----+
     ^             本 地 G 队 列                     ^              本 地 G 队 列
     |                                           |
  +--+---+              <---+                 +--+---+              <---+
  |      |       +-----+                      |      |       +-----+
  |  P   +<------+  G  |                      |  P   +<------+  G  |
  |      |       |     |                      |      |       |     |
  +--^---+       +--^--+                      +--^---+       +--^--+
     |              |                            |              |
  +--+--+        +--+--+                      +--+--+        +--+--+
  |  G  |        |  G  |                      |  G  |        |  G  |
  |     |        |     |                      |     |        |     |
  +-----+        +--^--+                      +-----+        +--^--+
                    |                                           |
                    |                                           |
                 +--+---+                                    +--+---+
                 |   G  |                                    |   G  |
                 |      |                                    |      |
                 +------+                                    +------+


                                                      全 局 队 列


             +-------+    +-------+    +-------+   +-------+
             |       |    |       |    |       |   |       |
             |   G   |    |   G   |    |   G   |   |   G   |
             |       |    |       |    |       |   |       |
             +-------+    +-------+    +-------+   +-------+

我们可以看到,每一个P都会有一个本地队列,保存着一系列的G,把G交给M来运行。

在新启动一个G的时候,golang会将G加入到一个P的队列中,等着被执行。

现在,用户启动了100w个goroutine,怎么办?如果用户只指定了两个P,那确实没办法,只好等着了。但是如果用户指定了较多的P呢?

现在系统检测到存在较多的G之后,会调用一个函数来创建一个新的M

1
void newm(void (*fn)(void), P *p)

但是这个M是有go的调度器来操作的,我们是没办法自己创建的。创建成功后,系统会给这个M分配一个P,接下来就是M和P的交互了。

P会从自己的队列里面拿出来一个G给M,让M执行。但是有可能这个P里面也是没有G的。那怎么办呢?

会首先检查全局队列,看看里面有没有G,放到自己的队列里面运行。如果全局队列里面没有,那就去其他P的队列里面抢一半的G过来,让自己运行。反正别的P里面的G也是运行不到,拿过来还能直接运行一会。

可是,也有可能一直获取不到新的G,那就证明确实没有几个G了,M就取消和P的关联,M进入空闲状态。

以上的前提是,系统中存在一个空闲的P,这个P没有和M绑定,这样系统可以创建新的M,使得更多的G可以被并发的执行。

我们提到,会有P从全局队列里面取G来运行,可是全局队列里面的G是怎么来的呢?

从刚刚我们提到的可以得知,新的协程会被存到P里面去,当协程越来越多的时候,本地P的队列就会满了,那么就会截取本地P一半的协程,扔到全局队里里面,留给其他的P使用。

此外,还有一种方式能够进入到全局队列中,我们稍后会提到。

调度

我们看到了一个P是怎么把G运行起来的,可是会存在一个情况,就是一个G在M上面运行,一直运行,不释放了!程序没有被阻塞的地方?这可怎么办?难道其他的协程要等到这个协程运行完才运行?那可不知道等到猴年马月啦!

系统在启动的时候,会启动一个后台监测程序sysmon,来监管着每一个程序的运行状态,如果发现一个协程运行的时间超过了10ms,会执行preemptone函数将当前协程的preempt设置为truestackguard0设置为stackPreempt

后续当这个协程发生外部程序调用时(非内联函数的使用),调度器检测到这个标志,就会将这个协程从M上取下来,让P选择其他的M使用。

具体能取下来的情况是什么呢?

  • IO 操作
  • Channel 阻塞
  • system call
  • 运行较长时间

当发生以上几种情况的时候,会把G停住,换个新的G来运行。

如果执行了一个内联函数,那么,这个G就不会下来了,因为这个程序根本没有往外面调用,所以,根本没有机会去停止,保存运行时,导致换不了。

现在假设在M0上的G运行了一个不释放CPU的操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
+--------------+       +--------------+                  +--------------+
|              |       |              |                  |              |
|              |       |              |                  |              |
|              |       |              |                  |              |
|     M0       |       |     M1       |                  |     M2       |
|              |       |              |                  |              |
|              |       |              |                  |              |
+------^-------+       +-------+------+                  +-------+------+
       |                       ^                                 ^
       |                       |                                 |
       |                       |                                 |
       |                   +---+-----+                       +---+-----+
       |                   |         |       +--------+      |         |       +--------+
       |                   |   P1    <-------+        |      |   P0    <-------+        |
       |                   |         |       |   G    |      |         |       |   G    |
   +---+----+              +---^-----+       |        |      +---^-----+       |        |
   |        |                  |             +--------+          |             +--------+
   |        |                  |                 ^+              |                 ^+
   |   G    |                +-+-----+            |            +-+-----+            |
   |        |                |       |       +----+---+        |       |       +----+---+
   +--------+                |   G   |       |        |        |   G   |       |        |
                             |       |       |   G    |        |       |       |   G    |
                             +-------+       |        |        +-------+       |        |
                                             +---^----+                        +---^----+
                                                 |                                 |
                                             +---+----+                        +---+----+
                                             |        |                        |        |
                                             |   G    |                        |   G    |
                                             |        |                        |        |
                                             +--------+                        +--------+

     +----------+         +----------+         +----------+          +----------+
     |          |         |          |         |          |          |          |
     |    G     ^         |    G     <---------+    G     +<---------+    G     |
     |          +---------+          |         |          |          |          |
     |          |         |          |         |          |          |          |
     +----------+         +----------+         +----------+          +----------+

通过这个操作后,在M0上面的P0会脱离挂载,找到一个新的M2,挂载上去,运行剩下的协程。

当M0上面的协程阻塞结束之后,G该怎么办呢?没有P没法运行呀!

那就会把G放到全局队列里面去,等着别的P来检出来。

相关结论

通过上面的一些介绍,尤其是画的神一般的ascii图,我们可以知道,P的数目是我们是可以手动指定的,默认就是CPU的个数。

当默认是多个P的时候,那么会有G挂靠在多个P上,也就是多个M上(为了方便描述,可以假定这几个协程是并行执行的)。所以,两个协程之间,并不是标准的阻塞,另一个才运行的。而是,只要有可用的M资源,就一定会执行起来。

所以,我们看开头的文章,在协程1往通道写入数据的时候,协程2已经不再阻塞,如果协程1协程2在两个P上面的话,那么实际是并行执行的。只是由于都是是用的stdout写出数据,才导致顺序是我们想要的结果。

我们再看一个考察点挺多的问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
	var i byte
	go func() {
		for i = 0; i <= 255; i++ {

		}
	}()

	runtime.Gosched()
	runtime.GC()
	fmt.Println("end")
}

这个程序,无论是单核还是多核,都永远不会结束。为什么呢?

首先byte相当uint8,所以,for循环始终执行,就是相当于

1
2
3
4
5
go func() {
		for {

		}
	}()

现在,我们强制执行了runtime.Gosched()让这个程序停止下,换给其他的协程使用,然后执行runtime.GC(),垃圾回收,结束程序执行。

我们知道,golang垃圾回收三色标记法,需要stop the world,stop the world的前提是能够把当前的状态全部保存起来。可是这个协程一直在M上运行,停不下来啊!

导致无法GC,卡住了。

如果我们把程序改成非内联的呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
	go func() {
		for  {
			fmt.Println("a")
		}
	}()

	runtime.Gosched()
	runtime.GC()
	fmt.Println("end")
}

完美退出。