Go语言闭包函数和goroutine实验

Go by 达达 at 2012-05-15

晚上做了几个关于闭包和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
}

实验就是提出猜想并验证猜想的过程,希望通过分享这次有趣的实验可以让大家获得关于实验的经验 :)