golang scheduler trick

一个常见的golang的调度的trap,主要的在使用for循环中生成新的goroutine的时候全局变量或者是局部变量的问题。

一个常见的调度trap

	array := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"}
var i = 0
for index, item := range array {
go func() {
fmt.Println("index:", index, "item:", item)
i++
}()
}
time.Sleep(time.Second * 1)
fmt.Println("------------------")
//output:
------------------
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
------------------

可以看到输出的结果都是8和i。似乎与我们的初衷不符,最初的意图是index与item每次为1,a;2,b;3,c;….这样,然后每次输出对应的结果。但显然,实际程序的执行流程并非是想我们所想的那样,关键是要明白,go func中的index与item分别表示的是什么,这里的go func每个index与item是共享的,并不是局部的,由于for循环的执行是很快的,每次循环启动一个go routine,在for循环结束之后(此时index与item的值分别变成了8与e),但是这个时候第一个启动的go routine可能还没有开始执行,由于它们是共享变量的,之后所有输出的index与item都是8与e于是出现了上面的效果。

关键问题是将全局的值变成局部的值,即是在每次go routine启动的时候有一个参数声明,比如每次在go routine启动的时候,传进来一个当时这个go routine 被调度到时候的index值,之后这个go routine再被调度到的时候,就能打印出当时的那个值,这样才是这个程序的真正意图,将之前的程序稍微修改下:

	length := len(array)
for i = 0; i < length; i++ {
go func(index int) {
//这里如果打印 array[i]的话 就会index out of range了 因为 i 是全局的(在执行到打印语句的时候 i的值已经变成了length+1了) 不是新启动的这个goroutine的
//新启动的goroutine与原来的main routine 是共享占空间的 因此 这个i也是共享的
fmt.Println("index:", index, "item:", array[index])
}(i)
}
time.Sleep(time.Second * 1)
fmt.Println("------------------")

//output
index: 0 item: a
index: 1 item: b
index: 2 item: c
index: 3 item: d
index: 4 item: e
index: 5 item: f
index: 6 item: g
index: 7 item: h
index: 8 item: i
------------------

根据上面的例子,可以看到,实际给go func传递参数的格式如下:

go func(形参名称 形参类型){...}(实参变量名称)

即使这样,go routine被调度的的先后顺序仍然是没法保证的,最多能做到的也只是在该go routine被调度到的时候,执行的数据正确而已,所以

不要对函数的执行时机做任何假设,除非你确实能够做出让这种假设成为绝对事实的保证。

使用runtime.Gosched

还有一种方式,是在执行这个函数的时候,相当于手动地让调度器进行了新的一轮的调度,这样就使得其他的goroutine可以有机会运行。

但是这样并不能100%保证被调度到的goroutine之前并没有被执行过。但是实际情况可能比较复杂,仅仅依靠重新调度这样的策略,可能还是会存在一定的随机性,不一定能满足我们的需求。

当然关于goroutine的使用知识还有很多,这里就是一个基本的注意点。

相关参考

《Go 并发编程实践》

推荐文章