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

协程引用循环变量的问题

作者: 千寻客 | 来源:发表于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

    相关文章

      网友评论

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

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