【Go語言踩坑系列(九)】Channel(上)

聲明

本系列文章並不會停留在Go語言的語法層面,更關注語言特性、學習和使用中出現的問題以及引發的一些思考。編程

咱們知道,Go實現了兩種併發形式,第一種是多線程共享內存,其實就是Java,C++等語言的多線程併發,經過鎖來進行訪問。另外一種則是Go特有的CSP(communicating sequential processes)併發模型。segmentfault

什麼是CSP?

CSP 是 Communicating Sequential Process 的簡稱,中文能夠叫作通訊順序進程,是一種併發編程模型,由 Tony Hoare 於 1977 年提出。它是在串行時代提出的一個概念,慢慢的演化成了如今的一種併發模型。簡單來講,CSP 模型由併發執行的實體(線程或者進程)所組成,實體之間經過發送消息進行通訊,這裏發送消息時使用的就是通道,或者叫 channel。那麼,CSP 模型的關鍵是關注 channel,而不關注發送消息的實體。而Go 語言實現了 CSP 部分理論,具體的模式以下圖所示。
channel1.png
Channel 在 gouroutine 間架起了一條管道,在管道里傳輸數據,實現 gouroutine 間的通訊;因爲它是線程安全的,因此用起來很是方便;channel 還提供 「先進先出」 的特性;它還能影響 goroutine 的阻塞和喚醒。安全

說到這,可能就有的同窗有些疑問,爲何要用channel,Goroutine不就能夠看做一個線程,而後線程間通訊用共享內存來通訊不行麼?請往下看。多線程

爲何要用channel

相信你們都聽過這麼一句話,Do not communicate by sharing memory; instead, share memory by communicating(不要經過共享內存來通訊,而要經過通訊來實現內存共享),這兩句話難道不是一個意思麼?從本質上來看,計算機上線程和協程同步信息其實都是經過共享內存來進行的,由於不管是哪一種通訊模型,線程或者協程最終都會從內存中獲取數據,因此更爲準確的說法是爲何咱們使用發送消息的方式來同步信息,而不是多個線程或者協程直接共享內存?
咱們從使用場景分析一下,首先,前半句應該是指咱們多應用於多線程通訊的方式,通常線程同步在線程間交換的信息僅僅是控制信息,好比某個A線程釋放了鎖,B線程能獲取到鎖並開始運行,這個不涉及數據的交換。數據的交換主要仍是經過共享內存(共享變量或者隊列)來實現,爲了保證數據的安全和正確性,共享內存就必須要加鎖等線程同步機制。而線程同步使用起來特別麻煩,容易形成死鎖,且過多的鎖會形成線程的阻塞以及這個過程當中上下文切換帶來的額外開銷。咱們一般會由於在代碼中加鎖而感到煩惱。
下半句呢?我理解後半句是說的channel來共享內存,在Go的這種方式中,要傳遞某個數據給另外一個goroutine(協程),能夠把這個數據封裝成一個對象,而後把這個對象的指針傳入某個channel中,另一個goroutine從這個channel中讀出這個指針,並處理其指向的內存對象。channel自己保證來同一時間只有一個goroutine能訪問channel的數據,就不用開發者去處理鎖了。
咱們根據他們的差別來總結一下:併發

  • 首先,使用發送消息來同步信息相比於直接使用共享內存和互斥鎖是一種更高級的抽象,使用更高級的抽象可以爲咱們在程序設計上提供更好的封裝,讓程序的邏輯更加清晰;
  • 其次,消息發送在解耦方面與共享內存相比也有必定優點,咱們能夠將線程的職責分紅生產者和消費者,並經過消息傳遞的方式將它們解耦,不須要再依賴共享內存;
  • 最後,Go 語言選擇消息發送的方式,經過保證同一時間只有一個活躍的線程可以訪問數據,可以從設計上自然地避免線程競爭和數據衝突的問題;

另外,是否是咱們都得使用channel來代替共享內存mutex,固然是不可能的,咱們在這來講明一個緣由:若是咱們向 Channel 中發送了一個指針而不是值的話,發送方在發送該條消息以後其實也保留了修改指針對應值的權利,若是這時發送方和接收方都嘗試修改指針對應的值,仍然會形成數據衝突的問題。固然這種大多數狀況下是一種設計上的問題,然而針對這種狀況使用更爲底層的互斥鎖纔是一種正確的方式。學習

固然,咱們會問channel怎麼保證同一時間只有一個活躍的線程可以訪問數據的呢?其實channel自己也是經過鎖來實現,這就對照咱們上邊所說的抽象的思想的結論了。具體是怎麼實現的呢?咱們將會在下一篇文章講述。spa

channel的不一樣種類以及常見的錯誤

channel分爲兩種,有緩衝channel和無緩衝channel。咱們經過下邊的代碼例子來區分不一樣的channel種類。線程

func main() {
    pipline := make(chan string) //構造無緩衝通道
    pipline <- "hello world" //發送數據
    fmt.Println(<-pipline)  //讀數據
}

運行會拋出錯誤,以下:設計

fatal error: all goroutines are asleep - deadlock!

思考一下,咱們建立的是一個無緩衝通道,而對於無緩衝通道,在接收者未準備好以前,發送操做是阻塞的。那麼,咱們該怎麼去解決這種問題呢?看下邊代碼。指針

func hello(pipline chan string)  {
    <-pipline
}

func main()  {
    pipline := make(chan string)
    go hello(pipline) //若是咱們換成直接在同一個協程裏讀數據會永遠阻塞
    pipline <- "hello world"
}

那麼咱們若是把這個例子改爲有緩衝通道還會阻塞嗎?咱們看下邊的例子:

func main() {
    pipline := make(chan string, 1)
    pipline <- "hello world"
    fmt.Println(<-pipline)
}

運行正常,此時是否是就能看出緩衝和沒有緩衝的區別呢?是的,區別在於在發送操做是否發生在有接受者時。那麼,對於有緩衝通道會發生什麼特殊狀況呢?

func main() {
    ch1 := make(chan string, 1)

    ch1 <- "hello world"
    ch1 <- "hello China"

    fmt.Println(<-ch1)
}

看這個例子,沒錯,他也會阻塞,每一個緩衝通道,都有容量,當通道里的數據量等於通道的容量後,此時再往通道里發送數據,就失形成阻塞,必須等到有人從通道中消費數據後,程序纔會往下進行
好比這段代碼,通道容量爲 1,可是往通道中寫入兩條數據,對於一個協程來講就會形成死鎖。

那麼問題來了,當程序一直在等待從通道里讀取數據,而此時並無人會往通道中寫入數據。此時程序就會陷入死循環,形成死鎖,咱們如何去解決呢?看下邊的例子:

func main() {
    pipline := make(chan string)
    go func() {
        pipline <- "hello world"
        pipline <- "hello China"
    }()
    for data := range pipline{
        fmt.Println(data)
    }
}

運行結果固然是all goroutines are asleep - deadlock!,通道沒有被關閉,程序就一直在等待讀取值,怎麼解決呢?

func main() {
    pipline := make(chan string)
    go func() {
        pipline <- "hello world"
        pipline <- "hello China"
        close(pipline) // 重點
    }()
    for data := range pipline{
        fmt.Println(data)
    }
}

注意看我標爲重點的地方,關閉通道,很明確的方法,既然問題是由於通道沒有被關閉形成的阻塞,那麼我在發送完數據後關掉就ok了啊~

下期預告

【Go語言踩坑系列(十)】Channel(下)

關注咱們

歡迎對本系列文章感興趣的讀者訂閱咱們的公衆號,關注博主下次不迷路~

Nosay

相關文章
相關標籤/搜索