Golang 並發簡介

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

並發概要

隨著多核CPU的普及, 為了更快的處理任務, 出現了各種並發編程的模型, 主要有以下幾種:

模型名稱 優點 缺點
多進程 簡單, 隔離性好, 進程間幾乎無影響 開銷最大
多線程 目前使用最多的方式, 開銷比多進程小 高併發模式下, 效率會有影響
非同步 相比多線程而言, 可以減少線程的數量 編碼要求高, 需要對流程分割合理
協程 使用者態線程, 不需要作業系統來調度, 所以輕量, 開銷極小 需要語言支援

協程介紹

協程是個抽象的概念, 可以映射到到作業系統層面的進程, 線程等概念.
由於協程是使用者態的線程, 不用作業系統來調度, 所以不受作業系統的限制, 可以輕鬆的建立百萬個, 因此也被稱為 "輕量級線程".

在 golang 中, 協程不是由庫實現的, 而是受語言層級支援的, 因此, 在 golang 中, 使用協程非常方便.
下面通過例子示範在 golang 中, 如何使用協程來完成並行作業.

golang 並發

實現方式

golang 中, 通過 go 關鍵字可以非常簡單的啟動一個協程, 幾乎沒有什麼學習成本.
當然並發編程中固有的業務上的困難依然存在(比如並發時的同步, 逾時等), 但是 golang 在語言層級給我們提供了優雅簡潔的解決這些問題的途徑.

理解了 golang 中協程的使用, 會給我們寫並發程式時帶來極大的便利.
首先以一個簡單的例子開始 golang 的並發編程.

package mainimport (     "fmt"     "time")func main() {     for i := 0; i < 10; i++ {             go sum(i, i+10)     }     time.Sleep(time.Second * 5)}func sum(start, end int) int {     var sum int = 0     for i := start; i < end; i++ {             sum += i     }     fmt.Printf("Sum from %d to %d is %d\n", start, end, sum)     return sum}

執行結果如下: (同時啟動10個協程做累加運算, 10個協程的執行順序可能會不一樣)

$ go run main.goSum from 0 to 10 is 45Sum from 6 to 16 is 105Sum from 7 to 17 is 115Sum from 2 to 12 is 65Sum from 8 to 18 is 125Sum from 1 to 11 is 55Sum from 9 to 19 is 135Sum from 3 to 13 is 75Sum from 4 to 14 is 85Sum from 5 to 15 is 95

通過 go 關鍵字啟動協程之後, 主進程並不會等待協程的執行, 而是繼續執行直至結束.
本例中, 如果沒有 time.Sleep(time.Second * 5) 等待5秒的話, 那麼主進程不會等待那10個協程的運行結果, 直接就結束了.
主進程結束也會導致那10個協程的執行中斷, 所以, 如果去掉 time.Sleep 這行代碼, 可能螢幕上什麼顯示也沒有.

簡單樣本

實際使用協程時, 我們一般會等待所有協程執行完成(或者逾時)後, 才會結束主進程, 但是不會用 time.Sleep 這種方式,
因為主進程並不知道協程什麼時候會結束, 沒法設定等待時間.

這時, 就看出 golang 中的 channel 機制所帶來的好處了. 下面用 channel 來改造上面的 time.Sleep

package mainimport "fmt"func main() {     var ch = make(chan string)     for i := 0; i < 10; i++ {             go sum(i, i+10, ch)     }     for i := 0; i < 10; i++ {             fmt.Print(<-ch)     }}func sum(start, end int, ch chan string) {     var sum int = 0     for i := start; i < end; i++ {             sum += i     }     ch <- fmt.Sprintf("Sum from %d to %d is %d\n", start, end, sum)}

程式執行結果和上面一樣, 因為是並發的緣故, 可能輸出的 sum 順序可能會不一樣.

$ go run main.goSum from 9 to 19 is 135Sum from 0 to 10 is 45Sum from 5 to 15 is 95Sum from 6 to 16 is 105Sum from 7 to 17 is 115Sum from 2 to 12 is 65Sum from 8 to 18 is 125Sum from 3 to 13 is 75Sum from 1 to 11 is 55Sum from 4 to 14 is 85

golang 的 chan 可以是任意類型的, 上面的例子中定義的是 string 型.
從上面的程式可以看出, 往 chan 中寫入資料之後, 協程會阻塞在那裡, 直到在某個地方將 chan 中的值讀取出來, 協程才會繼續運行下去.

上面的例子中, 我們啟動了10個協程, 每個協程都往 chan 中寫入了一個字串, 然後在 main 函數中, 依次讀取 chan 中的字串, 並在螢幕上列印出來.
通過 golang 中的 chan, 不僅實現了主進程 和 協程之間的通訊, 而且不用像 time.Sleep 那樣不可控(因為你不知道要 Sleep 多長時間).

並發時的緩衝

上面的例子中, 所有協程使用的是同一個 chan, chan 的容量預設只有 1, 當某個協程向 chan 中寫入資料時, 其他協程再次向 chan 中寫入資料時, 其實是阻塞的.
等到 chan 中的資料被讀出之後, 才會再次讓某個其他協程寫入, 因為每個協程都執行的非常快, 所以看不出來.

改造下上面的例子, 加入些 Sleep 代碼, 延長每個協程的執行時間, 我們就可以看出問題, 代碼如下:

package mainimport (     "fmt"     "time")func main() {     var ch = make(chan string)     for i := 0; i < 5; i++ {             go sum(i, i+10, ch)     }     for i := 0; i < 10; i++ {             time.Sleep(time.Second * 1)             fmt.Print(<-ch)     }}func sum(start, end int, ch chan string) int {     ch <- fmt.Sprintf("Sum from %d to %d is starting at %s\n", start, end, time.Now().String())     var sum int = 0     for i := start; i < end; i++ {             sum += i     }     time.Sleep(time.Second * 10)     ch <- fmt.Sprintf("Sum from %d to %d is %d at %s\n", start, end, sum, time.Now().String())     return sum}

執行結果如下:

$ go run main.goSum from 4 to 14 is starting at 2015-10-13 13:59:56.025633342 +0800 CSTSum from 3 to 13 is starting at 2015-10-13 13:59:56.025608644 +0800 CSTSum from 0 to 10 is starting at 2015-10-13 13:59:56.025508327 +0800 CSTSum from 2 to 12 is starting at 2015-10-13 13:59:56.025574486 +0800 CSTSum from 1 to 11 is starting at 2015-10-13 13:59:56.025593711 +0800 CSTSum from 4 to 14 is 85 at 2015-10-13 14:00:07.030611465 +0800 CSTSum from 3 to 13 is 75 at 2015-10-13 14:00:08.031926629 +0800 CSTSum from 0 to 10 is 45 at 2015-10-13 14:00:09.036724803 +0800 CSTSum from 2 to 12 is 65 at 2015-10-13 14:00:10.038125044 +0800 CSTSum from 1 to 11 is 55 at 2015-10-13 14:00:11.040366206 +0800 CST

為了示範 chan 的阻塞情況, 上面的代碼中特意加了一些 time.Sleep 函數.

  • 每個執行 Sum 函數的協程都會運行 10 秒
  • main函數中每隔 1 秒讀一次 chan 中的資料

從列印結果我們可以看出, 所有協程幾乎是同一時間開始的, 說明了協程確實是並發的.
其中, 最快的協程(Sum from 4 to 14…)執行了 11 秒左右, 為什麼是 11 秒左右呢?
說明它阻塞在了 Sum 函數中的第一行上, 等了 1 秒之後, main 函數開始讀出 chan 中資料後才繼續運行.
它自身運行需要 10 秒, 加上等待的 1 秒, 正好 11 秒左右.

最慢的協程執行了 15 秒左右, 這個也很好理解, 總共啟動了 5 個協程, main 函數每隔 1 秒 讀出一次 chan, 最慢的協程等待了 5 秒,
再加上自身執行了 10 秒, 所以一共 15 秒左右.

到這裡, 我們很自然會想到能否增加 chan 的容量, 從而使得每個協程儘快執行, 完成自己的操作, 而不用等待, 消除由於 main 函數的處理所帶來的瓶頸呢?
答案是當然可以, 而且在 golang 中實現還很簡單, 只要在建立 chan 時, 指定 chan 的容量就行.

package mainimport (     "fmt"     "time")func main() {     var ch = make(chan string, 10)     for i := 0; i < 5; i++ {             go sum(i, i+10, ch)     }     for i := 0; i < 10; i++ {             time.Sleep(time.Second * 1)             fmt.Print(<-ch)     }}func sum(start, end int, ch chan string) int {     ch <- fmt.Sprintf("Sum from %d to %d is starting at %s\n", start, end, time.Now().String())     var sum int = 0     for i := start; i < end; i++ {             sum += i     }     time.Sleep(time.Second * 10)     ch <- fmt.Sprintf("Sum from %d to %d is %d at %s\n", start, end, sum, time.Now().String())     return sum}

執行結果如下:

$ go run main.goSum from 0 to 10 is starting at 2015-10-13 14:22:14.64534265 +0800 CSTSum from 2 to 12 is starting at 2015-10-13 14:22:14.645382961 +0800 CSTSum from 3 to 13 is starting at 2015-10-13 14:22:14.645408947 +0800 CSTSum from 4 to 14 is starting at 2015-10-13 14:22:14.645417257 +0800 CSTSum from 1 to 11 is starting at 2015-10-13 14:22:14.645427028 +0800 CSTSum from 1 to 11 is 55 at 2015-10-13 14:22:24.6461138 +0800 CSTSum from 3 to 13 is 75 at 2015-10-13 14:22:24.646330223 +0800 CSTSum from 2 to 12 is 65 at 2015-10-13 14:22:24.646325521 +0800 CSTSum from 4 to 14 is 85 at 2015-10-13 14:22:24.646343061 +0800 CSTSum from 0 to 10 is 45 at 2015-10-13 14:22:24.64634674 +0800 CST

從執行結果可以看出, 所有協程幾乎都是 10秒完成的. 所以在使用協程時, 記住可以通過使用緩衝來進一步提高並發性.

並發時的逾時

並發編程, 由於不能確保每個協程都能及時響應, 有時候協程長時間沒有響應, 主進程不可能一直等待, 這時候就需要逾時機制.
在 golang 中, 實現逾時機制也很簡單.

package mainimport (     "fmt"     "time")func main() {     var ch = make(chan string, 1)     var timeout = make(chan bool, 1)     go sum(1, 10, ch)     go func() {             time.Sleep(time.Second * 5) // 5 秒逾時             timeout <- true     }()     select {     case sum := <-ch:             fmt.Print(sum)     case <-timeout:             fmt.Println("Sorry, TIMEOUT!")     }}func sum(start, end int, ch chan string) int {     var sum int = 0     for i := start; i < end; i++ {             sum += i     }     time.Sleep(time.Second * 10)     ch <- fmt.Sprintf("Sum from %d to %d is %d\n", start, end, sum)     return sum}

通過一個匿名函數來控制逾時, 然後同時啟動 計算 sum 的協程和timeout協程, 在 select 中看誰先結束,
如果 timeout 結束後, 計算 sum 的協程還沒有結束的話, 就會進入逾時處理.

上例中, timeout 只有5秒, sum協程會執行10秒, 所以執行結果如下:

$ go run main.goSorry, TIMEOUT!

修改 time.Sleep(time.Second * 5) 為 time.Sleep(time.Second * 15) 的話, 就會看到 sum 協程的執行結果

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.