美文网首页
协程引用循环变量的问题

协程引用循环变量的问题

作者: 千寻客 | 来源:发表于2019-07-07 21:42 被阅读0次

如果我们需要使用循环从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

相关文章

  • 协程引用循环变量的问题

    引 如果我们需要使用循环从0打印到9,每行一个数,我们可以用下面这样的Go代码完成 得到期望的结果,如下: 但是现...

  • Kotlin---使用协程的异步

    协程间的通信 协程与协程间不能直接通过变量来访问数据,会导致数据原子性的问题,所以协程提供了一套Channel机制...

  • Python-学习之路-16 协程

    协程 迭代器 可迭代(iterable):可直接作用for循环的变量 迭代器(iterator):不经可以作用域f...

  • python3 asyncio

    引入 asyncio 模块 定义一个协程函数 协程不能直接运行,把协程加入到事件循环(loop)。asyncio....

  • iOS底层-- weak修饰对象存储原理

    问题:为何weak修饰的变量可以打破循环引用?因为weak修饰的变量存储在散列表中的弱引用表里,不参与引用计数器的...

  • iOS 循环引用

    1. 循环应用的分类: 自循环引用; 相互循环引用; 多循环引用; 自循环引用: 一个对象中有一个成员变量A; 如...

  • __weak与__strong解决循环引用问题

    当block中涉及self以及self的成员变量时,就会造成循环引用问题。一般解决这类的循环引用是使用__weak...

  • 内存管理-循环引用

    三种类型循环引用 自循环引用 相互循环引用 多循环引用 自循环引用 假如有一个对象,内部强持有它的成员变量obj,...

  • python asyncio并发编程(6)

    1.单线程协程共享变量 在协程中多个协程任务共享一个变量时不会对变量早成影响,即不需要加锁,但前提是任务中没有aw...

  • kotlin 协程上下文那点事

    用线程做类比的话,协程的 context 可以认为是协程的“线程私有变量”,同时这个私有变量是不可变的。也就是说,...

网友评论

      本文标题:协程引用循环变量的问题

      本文链接:https://www.haomeiwen.com/subject/qjxihctx.html