淺談golang for 迴圈中使用協程的問題
兩個例子
package main import ( "fmt" "time" ) func Process1(tasks []string) { for _,task := range tasks { // 啟動協程併發處理任務 go func() { fmt.Printf("Worker start process task: %s\n",task) }() } } func main() { tasks := []string{"1","2","3","4","5"} Process1(tasks) time.Sleep(2 * time.Second) }
結果:
第一次執行
Worker start process task: 3 Worker start process task: 4 Worker start process task: 4 Worker start process task: 5 Worker start process task: 5
第二次執行
Worker start process task: 2 Worker start process task: 5 Worker start process task: 5 Worker start process task: 5 Worker start process task: 5
package main import ( "fmt" "time" ) func Process1(tasks []string) { for _,task) }() } } func Process2(tasks []string) { for _,task := range tasks { // 啟動協程併發處理任務 go func(t string) { fmt.Printf("Worker start process task: %s\n",t) }(task) } } func main() { tasks := []string{"1","5"} Process2(tasks) time.Sleep(2 * time.Second) }
結果
第一次執行
Worker start process task: 5 Worker start process task: 4 Worker start process task: 2 Worker start process task: 3 Worker start process task: 1
第二次執行
Worker start process task: 2 Worker start process task: 5 Worker start process task: 4 Worker start process task: 1 Worker start process task: 3
上述問題,有個共同點就是都引用了迴圈變數。即在for index,value := range xxx語句中,
index和value便是迴圈變數。不同點是迴圈變數的使用方式,有的是直接在協程中引用(題目一),有的作為引數傳遞(題目二)。
迴圈變數是易變的
首先,迴圈變數實際上只是一個普通的變數。
語句for index,value := range xxx中,每次迴圈index和value都會被重新賦值(並非生成新的變數)。
如果迴圈體中會啟動協程(並且協程會使用迴圈變數),就需要格外注意了,因為很可能迴圈結束後協程才開始執行,
此時,所有協程使用的迴圈變數有可能已被改寫。(是否會改寫取決於引用迴圈變數的方式)
迴圈變數需要繫結
在題目一中,協程函式體中引用了迴圈變數task,協程從被建立到被排程執行期間迴圈變數極有可能被改寫,所以會出現兩次結果相差較大,比如第一個協程啟動for range變數正好迴圈到3,for屬於主協程的一部分。go func是子協程,主子分開看。這種情況下,其實for range裡面的迴圈變數沒有跟子協程繫結,稱之為變數沒有繫結。所以,題目一列印結果是混亂的。很有可能(隨機)所有協程執行的task都是列表中的最後一個task,也可能不是。
在題目二中,協程函式體中並沒有直接引用迴圈變數task,而是使用的引數與協程進行了繫結。而在建立協程時,迴圈變數task
作為函式引數傳遞給了協程。引數傳遞的過程實際上也生成了新的變數,也即間接完成了繫結。
所以,題目二實際上是沒有問題的。就是實際引數順序是按照for range產生的變數順序繫結給子協程的。
ps:
簡單點來說
如果迴圈體沒有併發出現,則引用迴圈變數一般不會出現問題;
如果迴圈體有併發,則根據引用迴圈變數的位置不同而有所區別
通過引數完成繫結,則一般沒有問題;
函式體中引用,則需要顯式地繫結
補充:Go語言的協程中,寫死迴圈的注意點:
現象:
在寫Go的多協程程式時,出現過幾次無法理解的情況。
有一次,我想寫一個能跑滿cpu的程式,最容易想到的就是,開幾個Go的協程,每個協程裡寫死迴圈。沒想到,執行的時候發現,協程就只開出了一個。
另一次,我寫了個程式,也是開了多個協程。因為如果不阻塞住主函式,主函式一結束,程式就會結束。所以我就在主函式結束前加了個死迴圈。然後就發現整個協程都被卡住了。
分析:
其實,這個東西是協程的特點。以前沒用過協程,加上Go又說可以當執行緒用。所以想當然的寫了死迴圈。
準確的說,是在Go語言裡,寫了死迴圈,並且死迴圈內並沒有什麼系統呼叫,只有簡單的計算這類的。你就會發現,Go的協程排程就廢掉了。
協程並非像執行緒那樣,是由CPU中斷來觸發切換的。它不是應用程式能控制的(作業系統核心的某些關鍵操作會被保護,不被中斷)。即使你線上程裡寫了死迴圈,只要週期一到,CPU產生終端,死迴圈會被打斷,重新排程。但是,協程就不是這樣了,協程的排程其實是在協程呼叫了某個系統呼叫時,自動跳到另一個協程執行。也就是這個“中斷”是程式主動產生的,而不是被”中斷”。
所以,協程中,如果你寫了死迴圈,那你的死迴圈就會一直跑著,而不會讓別的協程執行。主函式中也是一樣,而且主函式中執行這個會讓整個協程卡住,因為排程的程式碼沒法被執行。
在Go語言中,如果你想寫死迴圈,迴圈裡面沒有系統呼叫,又想讓Go的協程能起作用,只需要在死迴圈裡面加一條語句即可。估計系統呼叫時也是這個語句起的作用。
runtime.Gosched() //主動讓出時間片
還可以使用
select{}
來實現無限阻塞,而不是使用for{}
以上為個人經驗,希望能給大家一個參考,也希望大家多多支援我們。如有錯誤或未考慮完全的地方,望不吝賜教。