golang 的协程调度

众所周知,golang 作为协程调度模型,是非抢占式而是自主放弃式的。

我的理解是,当一个协程进行 IO 的阻塞操作时,就会让出 CPU,让调度程序来调度其他协程来进行操作

调度程序并不会因为你的实际调用时间过长就干掉你,如果你觉得自己调用时间太长,可以用 runtime 库的 Gosched () 让出 CPU

但实际的测试(基于 1.7 版本)和之前的理解有差距,测试过程是递进的,可以直接跳过看结论

测试

协程是否独占直到自己运行完毕版本 1

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
package main

import (
"fmt"
"runtime"
"time"
)

var i int64
var j int64

func test1() {
for ;i < 1000000000; i++{
}
fmt.Println("test1",i,j,time.Now())
}

func test2() {
for ;j < 1000000000; j++{
}
fmt.Println("test2",i,j,time.Now())
}

func main() {
runtime.GOMAXPROCS(1)
go test1()
go test2()
for {
if runtime.NumGoroutine() <= 1 {
break
}
time.Sleep(time.Second)
}
}

这段代码执行两个协程,但是用 GOMAXPROCS 限定了只用单核

预期是 test1 运行完输出,然后执行 test2 再运行完输出

但实际结果是

1
2
test1 1000000000 1000000000 2017-07-24 09:51:41.577265937 +0800 CST
test2 1000000000 1000000000 2017-07-24 09:51:41.577769382 +0800 CST

然后我就误以为是在轮流执行

协程是否独占到运行完毕版本 2

随后我测试了这一段代码

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
package main

import (
"fmt"
"runtime"
"time"
)

var i int

func test1() {
for ;i < 1000000000; i++{
}
}

func test2() {
fmt.Println("test2",i)
}

func main() {
runtime.GOMAXPROCS(1)
go test1()
go test2()
for {
if runtime.NumGoroutine() <= 1 {
break
}
time.Sleep(time.Second)
}
}

输出

1
test2 1000000000

如果按照测试代码 1 的结论,两边轮流执行直到结束,那应该输出的是 test2, 0

然而现在并不是的,test1 执行完了才执行了 test2

于是我只能猜测 fmt.Println 函数会造成协程挂起,隐式的调用了 Gosched ()

这么一想,似乎就能说得通了,重看测试代码 1.1

test1 执行循环结束到 fmt 的时候挂起了,随后 test2 执行循环结束到 fmt 的时候又挂起了

此时 i,j 都是 1000000000,然后依次调用 test1 的 fmt 和 test2 的 fmt,输出相同的时间和 i,j

但是新的问题又来了,时间为什么会相同呢?调用 fmt 会挂起,通过测试代码 1.2 应该能确认了

但是对于函数调用来说,参数会比函数先运行,因此测试代码 1.1 的两个函数的 time.Now () 记录的时间值,不应该是一样的

难道。。time.Now () 函数也会引起挂起??

time.Now () 函数是否会引起挂起

这一次的测试,在 1.2 的函数 test1 循环体中加入 time.Now ()

1
2
3
4
5
func test1() {
for ;i < 1000000000; i++{
time.Now()
}
}

运行结果

1
test2 493747

果然切换了,因为这里的 i 是 493747(为什么不是 0?稍后讨论),并且程序执行了很久才退出

这就说明 test1 执行了一半去执行了 test2

这就很奇怪了,如果说 fmt 引起和标准输出的交互,从而导致切换也就算了

time.Now () 凭什么切,难道申请内存就会切换??

申请内存是否会切换

这一次测试,在 1.2 的函数 test1 循环体中加入了申请内存

1
2
3
4
5
6
7
func test1() {
var buf []byte
for ;i < 1000000000; i++{
buf = make([]byte,1)
}
fmt.Println(buf)
}

输出

1
2
test2 957470
[0]

果然这一次也切换了,验证了申请内存也会导致切换

但是切换的 i 不是 1,也就是说并不是每一次调用都会切换。而是按照时间来的。

单协程运行一段时间后,如果调用申请内存操作,就会被调度程序切换协程

测试调度程序切换时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var t time.Time

func test1() {
t = time.Now()
var buf []byte
for ;i < 1000000000; i++{
buf = make([]byte,1)
}
fmt.Println(buf)
}

func test2() {
fmt.Println("test2",i,time.Since(t))
}

输出基本是 10ms 以上波动一点点

总结

  • 对一段纯计算的代码,调度程序并不会因为你的实际调用时间过长就干掉你,如果你觉得自己调用时间太长,可以用 runtime 库的 Gosched () 让出 CPU

  • (基于 1.7 版本) 你在计算的代码中申请了内存,那么如果已经运行了超过 10ms,就可能被调度程序切换走,这也是为了防止饥饿吧