【譯】並發、協程和GOMAXPROCS

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

####介紹

當有新人加入Go-Miami組,他們經常希望去學習的並行存取模型。當我剛剛開始聽說Go語言的時候,並發似乎看起來是這門語言的熱門詞彙。當我看了Rob Pike的Go併發模式的視頻之後,我認為我應該去學習這門語言了。

為了去理解是如何通過Go語言編寫出更簡單、難出錯的並發程式,我們首先需要去理解什麼是並發程式和並發程式的結果是什麼這兩個問題。我不會去講述CSP(通訊順序進程、Communicating Sequential Processes),這是Go語言實現管道(channel)的基礎。這篇文章將著重講並發是什麼,協程的角色是什麼,環境變數GOMAXPROCS和運行時函數如何影響Go語言的程式的執行。

####進程和線程

當我們運行一個應用,比如我寫這篇文章用的瀏覽器,作業系統會為這個應用建立一個進程。進程的任務就像是一個容器,為這個應用啟動並執行時候使用和管理資源。這些資源套件括記憶體位址空間、指向檔案的控制代碼、裝置和線程。

線程是執行的一部分,它會被作業系統調度去執行進程裡面、被我們在函數裡面實現的代碼。一個進程開始於一個線程,也叫做主線程,當這個線程結束整個進程也會隨之結束。這是因為主線程是應用程式的來源。主線程可以反過來建立更多的線程,而這些線程還能建立更多的線程。

作業系統可以調度一個線程在一個可用的處理器上面執行,而不管處理這個線程的父線程在哪個處理器上面執行。每個作業系統都有自己的調度演算法,並不是特定的一種,來決定最好的方式讓我們開發並行程式。並且這些演算法將會隨著作業系統的每次升級發生改變,這個需要我們注意。

####協程和並行

Go語言裡面任何函數或者方法可以建立一個協程。我們可以認為主方法執行起來是一個協程,然而Go的運行時並沒有啟動這個協程。協程可以認為是輕量級的,因為它們佔用很小的記憶體和資源,並且它初始化的棧空間也很小。早先的Go語言1.2版本棧空間會被初始化為4K,目前的1.4版本會被初始化為8K。這個棧可以隨著需要自動增加空間。

作業系統調度可用的處理器來執行線程,Go的運行時會通過一個作業系統線程來調度協程。預設情況下,Go的運行時會分配一個邏輯處理器來執行代碼裡面定義的全部協程。甚至通過這一個邏輯處理器和作業系統線程,成百上千個協程可以被並發的高效迅速地調用執行。不建議添加更多的邏輯處理器,但如果你想在並行運行協程,Go提供了通過GOMAXPROCS環境變數或運行時函數來增加邏輯處理器。

並發並不是並行。並行是在多核處理器不能的核同時並行得執行兩個或兩個以上的線程。如果你配置運行時的邏輯處理器多於1個,調度器將會在這些邏輯處理器上面分配協程,這樣做的結果就是在不同的作業系統線程上面執行多個協程。然而,想要真正的實現並存執行你的伺服器必須是多核處理器。如果不是,即使Go的運行時被設定為需要多個邏輯處理器,協程仍會並發的在同一個處理處理器上面執行協程。

####並發的例子

我們來建立一個小程式來展示Go並發執行協程s。這個例子我們是在預設設定下面執行,預設設定是要使用一個邏輯處理器。

package mainimport (    "fmt"    "sync")func main() {    var wg sync.WaitGroup    wg.Add(2)    fmt.Println("Starting Go Routines")    go func() {defer wg.Done()for char := 'a'; char < 'a'+26; char++ {    fmt.Printf("%c ", char)}    }()    go func() {defer wg.Done()for number := 1; number < 27; number++ {    fmt.Printf("%d ", number)}    }()    fmt.Println("Waiting To Finish")    wg.Wait()    fmt.Println("\nTerminating Program")}

這個程式通過關鍵字go啟動了兩個協程,聲明了兩個匿名函數。第一個協程列印了小寫英語字母表,第二個列印了1到26這些數字。當我們運行這個程式,將會得到以下的結果:

Starting Go RoutinesWaiting To Finisha b c d e f g h i j k l m n o p q r s t u v w x y z 1 2 3 4 5 6 7 8 9 10 1112 13 14 15 16 17 18 19 20 21 22 23 24 25 26Terminating Program

我們來看一下輸出的結果,代碼被並發的執行。一旦這兩個協程被啟動,主的協程將會等待這兩個協程執行結束。我們需要用這個方法否則一旦主協程運行結束,整個程式就結束了。WaitGroup是一個不錯的方來用來在不同的協程之間通訊是否完成。

我們可以發現第一個協程完整地展示了26個字母之後,第二個協程才開始展示26個數字。因為這兩個協程執行速度太快,毫秒時間內就能執行結束,我們並沒有能夠看到是否調度器在第一個協程執行完之前中斷了它。我們可以在第一個協程裡面增加一個等待時間來判斷調度器的策略。

package mainimport (    "fmt"    "sync"    "time")func main() {    var wg sync.WaitGroup    wg.Add(2)    fmt.Println("Starting Go Routines")    go func() {defer wg.Done()time.Sleep(1 * time.Microsecond)for char := 'a'; char < 'a'+26; char++ {    fmt.Printf("%c ", char)}    }()    go func() {defer wg.Done()for number := 1; number < 27; number++ {    fmt.Printf("%d ", number)}    }()    fmt.Println("Waiting To Finish")    wg.Wait()    fmt.Println("\nTerminating Program")}

這次我們在第一個協程裡面增加了1秒鐘的等待時間,調用sleep導致了調度器交換了兩個協程的執行順序:

Starting Go RoutinesWaiting To Finish1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ab c d e f g h i j k l m n o p q r s t u v w x y zTerminating Program

這次數字在字母前面。sleep會影響調度器停止第一個協程,先執行第二個協程。

####並行的例子

我們上面的兩個例子協程都是在並發執行,並不是並行的。我們改變一下代碼來允許代碼並存執行。我們需要做的就是為調度器增加一個邏輯處理器讓他能夠使用兩個線程:

package mainimport (    "fmt"    "runtime"    "sync")func main() {    runtime.GOMAXPROCS(2)    var wg sync.WaitGroup    wg.Add(2)    fmt.Println("Starting Go Routines")    go func() {defer wg.Done()for char := 'a'; char < 'a'+26; char++ {    fmt.Printf("%c ", char)}    }()    go func() {defer wg.Done()for number := 1; number < 27; number++ {    fmt.Printf("%d ", number)}    }()    fmt.Println("Waiting To Finish")    wg.Wait()    fmt.Println("\nTerminating Program")}

這是程式的輸出:

Starting Go RoutinesWaiting To Finisha b 1 2 3 4 c d e f 5 g h 6 i 7 j 8 k 9 10 11 12 l m n o p q 13 r s 14t 15 u v 16 w 17 x y 18 z 19 20 21 22 23 24 25 26Terminating Program

每一次我們運行這個程式將會得到不同的結果。調度器並不會每一次都執行得到相同的結果。我們可以看到協程這次真的是並存執行了。兩個協程立刻開始運行,並且你可以看到兩個都在競爭輸出各自的結果。

####結論

我們可以為調度器增加多個邏輯處理器,但這不意味著我們必須要這麼做。Go團隊把運行時的並行數預設設為1是有原因的。隨意添加邏輯處理器和並行的協程並不會一定為你的程式提供更好的效能。要做好效能壓力測試,確保修改Go運行時GOMAXPROCS一定能最佳化效能時再這麼做。

在我們的應用裡面建立並發的協程,這個問題最終將會導致我們的協程可以同時嘗試訪問同樣的資源。讀寫共用資源一定要是原子性的。換句話說,讀和寫操作一定要在一個協程裡面同一個時刻只有一個操作被執行,否則我們就需要在代碼裡面建立臨界條件。學習更多的競爭條件可以讀這篇文章。

管道是Go語言裡面安全和優雅的編寫並發程式的方法,它消除了編寫競爭條件,可以使得開發並行程式更加有趣。我們現在已經知道協程是如何工作、被調度和如果並存執行,接下來我們將會講述管道。

######參考文獻1. Concurrency, Goroutines and GOMAXPROCS2. 並發和並行的區別:吃饅頭的比喻

原文連結:【譯】並發、協程和GOMAXPROCS,轉載請註明來源!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.