協(xié)程引用循環(huán)變量的問題

如果我們需要使用循環(huán)從0打印到9,每行一個數(shù),我們可以用下面這樣的Go代碼完成

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

得到期望的結(jié)果,如下:

0
1
2
3
4
5
6
7
8
9

但是現(xiàn)實中我們往往需要使用異步并發(fā)處理來提高性能,比如循環(huán)中可能是一個很耗時的邏輯。而這個時候就很容易出現(xiàn)問題了。

協(xié)程引用循環(huán)變量的坑

循環(huán)體中啟動協(xié)程異步執(zhí)行,這個時候就容易出現(xiàn)問題了,比如下面這樣一段代碼就會出現(xiàn)我們不期望的結(jié)果。

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

我們期望他能亂序輸出09這幾個數(shù),但是他的執(zhí)行結(jié)果并非如此。實際的執(zhí)行結(jié)果如下:

7
10
10
10
10
10
10
10
10
7

可以看到他的執(zhí)行結(jié)果大家基本都輸出10。其實原因也很容易解釋:

主協(xié)程的循環(huán)很快就跑完了,而各個協(xié)程才開始跑,此時i的值已經(jīng)是10了,所以各協(xié)程都輸出了10。(輸出7的兩個協(xié)程,在開始輸出的時候主協(xié)程的i值剛好是7,這個結(jié)果每次運行輸出都不一樣)

這是一個初學者很容易出現(xiàn)的問題,還比較隱晦難以發(fā)現(xiàn)。

原因與解決辦法

出現(xiàn)這個問題最主要的原因是Golang中允許啟動的協(xié)程中引用外部的變量。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也會提示錯誤,我們很容易發(fā)現(xiàn)。
      System.out.println(finalI); 
    }
  })
}

所以Java中只能寫一個臨時變量finalI來供異步任務使用,這樣每個異步任務都會拿到當時i的一個snapshot。

Go代碼也能改成類似的代碼使運行出正確的結(jié)果

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

運行結(jié)果為

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已經(jīng)有了一個副本,協(xié)程中針對副本處理。

工具

這個問題Golang雖然沒有在語言層面上像Java一樣要求使用final變量,但是他也提供了一個代碼檢查工具go vet能發(fā)現(xiàn)這個問題:

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

我們可以將這個工具集成到IDE中,讓我們在寫代碼的時候能自動對代碼進行檢查,用于快速發(fā)現(xiàn)這類的問題。

Goland設(shè)置

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

image.png
image.png

有了這樣的設(shè)置之后,后續(xù)編輯代碼的時候,他就能自動檢查出這類問題,提示我們可能存在的問題。

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出現(xiàn)與java8,見accessing-members-of-enclosing-class?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容