协程引用循环变量的问题

千寻客 · · 4156 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

如果我们需要使用循环从0打印到9,每行一个数,我们可以用下面这样的Go代码完成

for i := 0; i < 10; i++ {
  fmt.Println(i)
}

得到期望的结果,如下:

0
1
2
3
4
5
6
7
8
9

但是现实中我们往往需要使用异步并发处理来提高性能,比如循环中可能是一个很耗时的逻辑。而这个时候就很容易出现问题了。

协程引用循环变量的坑

循环体中启动协程异步执行,这个时候就容易出现问题了,比如下面这样一段代码就会出现我们不期望的结果。

for i := 0; i < 10; i++ {
  go func() {
    fmt.Println(i)
  } ()
}

我们期望他能乱序输出09这几个数,但是他的执行结果并非如此。实际的执行结果如下:

7
10
10
10
10
10
10
10
10
7

可以看到他的执行结果大家基本都输出10。其实原因也很容易解释:

主协程的循环很快就跑完了,而各个协程才开始跑,此时i的值已经是10了,所以各协程都输出了10。(输出7的两个协程,在开始输出的时候主协程的i值刚好是7,这个结果每次运行输出都不一样)

这是一个初学者很容易出现的问题,还比较隐晦难以发现。

原因与解决办法

出现这个问题最主要的原因是Golang中允许启动的协程中引用外部的变量。Java对这类问题的解决方式比较合理,它也允许异步任务引用外部变量,但是要求外部变量必须是final或者是effective final[1]

for (int i = 0; i < 10; i++) {
  final int finalI = i;
  new Thread(new Runnable() {
    public void run() {
      // 这儿要求使用变量finalI,
      // 如果使用i,就会报编译错误,
      // 而且一般IDE也会提示错误,我们很容易发现。
      System.out.println(finalI); 
    }
  })
}

所以Java中只能写一个临时变量finalI来供异步任务使用,这样每个异步任务都会拿到当时i的一个snapshot。

Go代码也能改成类似的代码使运行出正确的结果

for i := 0; i < 10; i++ {
    i0 := i
    go func() {
        fmt.Println(i0)
    } ()
}

运行结果为

1
7
2
9
0
3
4
8
6
5

其实Golang推荐其他更简洁的写法

for i := 0; i < 10; i++ {
    go func(i0 int) {
        fmt.Println(i0)
    } (i) // 
}
// 或者
for i := 0; i < 10; i++ {
  // 这一段代码相当与下面这样的一段伪码
  // routine = makeroutine(fmt.Println, i)
  // start(routine)
  // 于是routine中的i值是一个副本
  go fmt.Println(i) 
}

这两个写法其实与前面java代码中用临时变量的原理是一样的,即变量i已经有了一个副本,协程中针对副本处理。

工具

这个问题Golang虽然没有在语言层面上像Java一样要求使用final变量,但是他也提供了一个代码检查工具go vet能发现这个问题:

$ go vet main.go
main.go:24:16: loop variable i captured by func literal

我们可以将这个工具集成到IDE中,让我们在写代码的时候能自动对代码进行检查,用于快速发现这类的问题。

Goland设置

Goland中可以在 Preferences / Tools / File Watchers中添加一个golangci-lint的工具

image.png
image.png

有了这样的设置之后,后续编辑代码的时候,他就能自动检查出这类问题,提示我们可能存在的问题。

golangci-lint run --disable=typecheck demo
main.go:12:16: loopclosure: loop variable i captured by func literal (govet)
            fmt.Println(i)
                        ^

参考信息

https://github.com/golang/go/wiki/CommonMistakes


  1. effective final出现与java8,见accessing-members-of-enclosing-class


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:千寻客

查看原文:协程引用循环变量的问题

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

4156 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传