C/Go Routine 的异步闭包问题

Created Tue, 26 Sep 2023 17:40:47 +0800 Modified Sat, 04 Nov 2023 00:34:56 +0800
570 Words

参考文章LoopvarExperiment

从一道经典的js面试题说起

1
2
3
4
5
6
7
8
9
for (var i= 0 ; i <3 ;i++){
    setTimeout(()=>{
        console.log(i)
    })
}
// output: 
// 3 
// 3 
// 3

由于var的作用域与setTimeout的异步执行顺序问题. 将闭包函数同步推入异步队列, 异步执回调行时取i值为同步循环后的最终值, 即跳出循环的值3

golang (在go version 1.21.1下测试)中也有类似的问题

for的声明语句中定义的变量会有值状态丢失的情况, 包括双分号和range语法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func main() {
  done := make(chan bool)
  for i := 0; i < 3; i++ {
    go func() {
      fmt.Println(i)
      done <- true
    }()
  }
  // chan blocks the code to reach the end before all the routines are done
  for i := 0; i < 3; i++ {
    <-done
  }
}
// output: 
// 3
// 3
// 3

同时借此可以类比拓展出其他类似js同异步打印问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

///...
go func() {
    fmt.Println("second")
    done <- true
}()
fmt.Println("first")
///...
// output: 
// first 
// second

推测也是go routine同异步导致的问题, go关键字将回调开启并发, 也可以看作成一种异步.

但是 golang 中 go routine 不是异步队列模型, 所以不会保证输出顺序

解决方式也差不多, 本质上指定一个循环体局部变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
for i := 0; i < 3; i++ {
  val := i
  go func() {
    fmt.Println(val)
    done <- true
  }()
  /* alternative 
  go func(i int) {
    fmt.Println(i)
    done <- true
  }(i)
  */
}
// output:
// 2 
// 1 
// 0 // not strictly ordered. can be  2 0 1 ....

这个问题在官方wiki上被称作为loopvar, 1.21会有一个同名实验特性用来避免这个问题, 并且这个特性拟在1.22 默认开启

1
2
#add env variable when executing go commands
GOEXPERIMENT=loopvar go run main.go

输出结果:

对比