golang的协程调度

原创内容,转载请注明出处

Posted by Weakyon Blog on July 21, 2017

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

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

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

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

1 测试

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

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再运行完输出

但实际结果是

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

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

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

随后我测试了这一段代码

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)
    }
}

输出

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()函数也会引起挂起??

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

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

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

运行结果

test2 493747

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

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

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

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

1.4 申请内存是否会切换

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

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

输出

test2 957470
[0]

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

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

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

1.5 测试调度程序切换时间

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以上波动一点点

2 总结

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

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

21 Jul 2017