Go语言闭包函数和goroutine实验
晚上做了几个关于闭包和goroutine的实验,实验的目的是验证一个设计上的猜想:假设系统中有一处地方是通过发送匿名函数给一个goroutine的方式执行的,那么如果发送消息后不用一个channel等待反馈,光靠闭包机制修改局部变量来反映执行结果的话,有可能照成不可预期的结果。
第一个实验是最简单的闭包实验,验证依靠闭包机制可以修改函数的局部变量:
package main import "fmt" func main() { x := 0 exec(func() { x += 100 }) fmt.Println(x) } func exec(callback func()) { callback() }
实验效果正如预期,代码编译通过,x最后值是100。
第二个实验复杂一些,先创建一个goroutine执行loop函数,等待来自频道c的消息,main函数所在的goroutine则将匿名函数通过频道c发送给loop所在的goroutine执行:
package main import "fmt" import "math/rand" func main() { c = make(chan func()) go loop() x := 0 y := rand.Int() % 1000000 exec(func() { for i := 0; i < y; i++ { x += 1 } }) fmt.Println(x) fmt.Println(y) } var c chan func() func loop() { for { select { case callback := <-c: callback() } } } func exec(callback func()) { c <- callback }
这次实验用了随机数来做循环条件,目的是避免编译器优化循环,但是实验结果还是不像预期的一样,x值始终和y是一致的。按照最初的预计,x应该在还没被修改或只被部分修改的时候,main函数就执行到了Println的地方,这时候x的值应该不等于y的值。
为什么x始终和y一致呢?查找Go的官方文档也没说goroutine遇到闭包函数的时候会有这种高档功能,如果确定有这种高档功能,那我上面提到的情况就好设计了,靠修改局部变量就可以返回结果。
我又做了几个实验,其中一则实验我猜想如果上一个实验是正确的,那么Go应该有内置的某种机制保持让loop所在的goroutine执行完毕之前让main所在的goroutine不继续执行下去,于是我多加了一个频道b,让loop在收到来自频道c的消息后接着堵塞在等待来自频道b的消息,如果猜想正确的话,运行结果应该是程序出错退出,并提示所有进程陷入休眠照成死锁,因为main应该等loop返回,而loop又在等main发消息。
代码如下:
package main import "fmt" import "math/rand" func main() { c = make(chan func()) b = make(chan int) go loop() x := 0 y := rand.Int() % 1000000 exec(func() { for i := 0; i < y; i++ { x += 1 } }) fmt.Println(x) fmt.Println(y) } var c chan func() var b chan int func loop() { for { select { case callback := <-c: <-b callback() } } } func exec(callback func()) { c <- callback b <- 1 }
但是这次实验反而正确执行通过并得到本来第二个实验预期的结果,x的值等于0,屡试不爽。
为什么呢?最后我想到,Go的调度器不是公平调度的,loop所在的goroutine收到消息的时候main所在的goroutine正在休眠,需要等loop执行完陷入下次等待消息的时候才会让出调度器,main才会继续下去,所以实验二没得到猜想的结果,因为猜想中把Go的调度器想成公平调度的了,而实验三正好让loop陷入一次等待,让出了调度器。
为了验证想法,我给loop加了一句runtime.Gosched(),强制让出调度器,这时候实验就如预期一样了,x的值还是0的时候main已经返回。
package main import "fmt" import "runtime" import "math/rand" func main() { c = make(chan func()) go loop() x := 0 y := rand.Int() % 1000000 exec(func() { for i := 0; i < y; i++ { x += 1 } }) fmt.Println(x) fmt.Println(y) } var c chan func() func loop() { for { select { case callback := <-c: runtime.Gosched() callback() } } } func exec(callback func()) { c <- callback }
实验就是提出猜想并验证猜想的过程,希望通过分享这次有趣的实验可以让大家获得关于实验的经验 :)