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,就可能被调度程序切换走,这也是为了防止饥饿吧