聊一聊Go中channel的行爲

簡介

當我第一次開始使用Go的channel的時候,我犯了一個錯誤,認爲channel是一個數據結構。我將channel看做是在goroutine之間提供自動同步訪問的隊列。這種結構上的理解使我編寫了許多糟糕而複雜的併發代碼。算法

隨着時間的推移,我逐漸瞭解到,最好的辦法是忘掉channel的結構,關注它們的行爲。因此提到channel,我想到了一個概念:信號。一個通道容許一個goroutine向另外一個goroutine發出特定事件的信號。信號是應該使用channel作的一切的核心。將channel看做一種信號機制,可讓你編寫具備明肯定義和更精確的行爲的代碼。數據結構

要理解信號是如何工做的,咱們必須理解它的三個屬性:併發

  • 交付保證
  • 狀態
  • 有數據或無數據

這三個屬性共同構成了圍繞信號的設計理念。 在討論這些屬性以後,咱們將提供一些代碼示例,這些示例演示如何使用這些屬性進行信號傳遞。函數

交付保證

交付保證是基於一個問題:「我是否須要保證由特定的goroutine發送的信號已經被收到了?」spa

換句話說,咱們能夠給出下面這個示例:設計

01 go func() {
02     p := <-ch // 接收
03 }()
04
05 ch <- "paper" // 發送

執行發送的goroutine是否須要保證,經過第5行被髮送的一份報告(paper),在繼續執行以前,被第2行要接收的goroutine接收到了?code

根據這個問題的答案,你會知道使用兩種channel中的哪種:無緩衝或緩衝。每種channel在交付保證時提供不一樣的行爲。隊列

保證是很重要的。好比,若是你沒有生活保證時,你不會緊張嗎?在編寫併發軟件時,對是否須要保證有一個重要的理解是相當重要的。隨着咱們的繼續,你將學會如何作出決定。事件

狀態

channel的行爲直接受其當前狀態的影響。channel的狀態能夠爲nilopenclosed同步

如下示例將介紹,如何在這三個狀態中聲明或設置一個channel。

// ** nil channel

// 若是聲明爲零值的話,將會是nil狀態
var ch chan string

// 顯式的賦值爲nil,設置爲nil狀態
ch = nil


// ** open channel

// 使用內部函數make建立的channel,爲open狀態
ch := make(chan string)    


// ** closed channel

// 使用close函數的,爲closed狀態
close(ch)

狀態決定了發送接收操做的行爲。

信號經過一個channel發送和接收。不能夠稱爲讀/寫,由於channel不執行輸入/輸出。

當一個channel爲nil狀態時,channel上的任何發送或接收都將被阻塞。當爲open狀態時,信號能夠被髮送和接收。若是被置爲closed狀態的話,信號不能再被髮送,但仍有可能接收到信號。

有數據或無數據

須要考慮的最後一個信號屬性是,信號是否帶有數據。

經過在channel上執行發送帶有數據的信號。

01 ch <- "paper"

當你用數據發出信號時,一般是由於:

  • goroutine被要求開始一項新任務。
  • goroutine報告了一個結果。

經過關閉一個channel來發送沒有數據的信號。

01 close(ch)

當發送沒有數據信號的時候,一般是由於:

  • goroutine被告知要中止他們正在作的事情。
  • goroutine報告說已經完成,沒有結果。
  • goroutine報告說它已經完成了處理,而且關閉。

沒有數據的信號傳遞的一個好處是,一個單一的goroutine能夠同時發出不少的信號。而在goroutines之間,用數據發送信號一般是一對一之間的交換。

有數據信號

當要使用數據進行信號傳輸時,您能夠根據須要的擔保類型選擇三種channel配置選項。

這三個channel選項是無緩衝,緩衝>1或緩衝=1。

  • 有保證
    • 一個沒有緩衝的通道能夠保證發送的信號已經收到。
      • 由於信號的接收在信號發送完成以前就發生了。
  • 無保證
    • 一個大小>1的緩衝通道不能保證發送的信號已經收到。
      • 由於信號的發送是在信號接收完成以前發生的。
  • 延遲保證
    • 一個大小=1的緩衝通道爲您提供了一個延遲的保證。它能夠保證發送的前一個信號已經收到。
      • 由於第一個信號的接收,在第二個信號發送完成以前就發生了。

緩衝區的大小毫不是一個隨機數,它必須是爲一些定義好的約束而計算出來的。在計算中沒有無窮遠,全部的東西都必須有一個明肯定義的約束,不管是時間仍是空間。

無數據信號

沒有數據的信號,主要是爲取消而預留的。它容許一個goroutine發出信號,讓另外一個goroutine取消他們正在作的事情,而後繼續前進。取消可使用非緩衝和緩衝channel來實現,可是在沒有數據發送的狀況下使用緩衝channel會更好。

內置函數 close 用於在沒有數據的狀況下發出信號。正如狀態一節中介紹的,你仍然能夠在一個關閉的通道接收到信號。事實上,在一個關閉的channel上的任何接收都不會阻塞,接收操做老是返回。

在大多數狀況下,您但願使用標準庫 context 包來實現無數據的信號傳遞。context 包使用一個沒有緩衝的channel來進行信號傳遞,而內置函數 close 發送沒有數據的信號。

若是選擇使用本身的通道進行取消,而不是 context 包,那麼你的通道應該是 chan struct{} 類型的。這是一種零空間的慣用方式,用來表示一個僅用於信號傳輸的channel。

場景

有了這些屬性,進一步瞭解它們在實踐中工做的最佳方式就是運行一系列的代碼場景。

有數據信號 - 保證 - 無緩衝的channel

當你須要知道發送的信號已經收到時,就會有兩種狀況出現。一種是等待任務,另外一種是等待結果。

場景 1 - 等待任務

設想你是一名經理,並僱傭了一名新員工。在這個場景中,你但願你的新員工執行一個任務,可是他們須要等待,直到你準備好。這是由於你須要在他們開始以前給他們一份報告(paper)。

01 func waitForTask() {
02     ch := make(chan string)
03
04     go func() {
05         p := <-ch
06
07         // 員工執行工做
08
09         // 員工能夠自由地去作
10     }()
11
12     time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
13
14     ch <- "paper"
15 }

在02行中,無緩衝的channel被建立,string 類型的數據將被髮送到信號中。而後在04行,一名員工被僱傭,並被告知在05行前等待你的信號,而後再作他們的工做。第05行是channel接收,致使員工在等待你發送的文件時阻塞。一旦員工收到了這份報告,員工就完成了工做,而後就能夠自由地離開了。

你做爲經理正在和你的新員工一塊兒工做。所以,當你在第04行僱傭了員工後,你會發現本身(在第12行)作了你須要作的事情來解阻塞而且通知員工。值得注意的是,不知道要花費多長的時間來準備這份報告(paper)。

最終你準備好給員工發信號了。在第14行,你執行一個帶有數據的信號,數據就是那份報告(paper)。因爲使用了一個沒有緩衝的channel,因此當你的發送操做完成後,你就獲得了該僱員已經收到該文件的保證。接收發生在發送以前。

場景2 - 等待結果

在接下來的場景中,事情發生了反轉。這一次,你但願你的新員工在被僱傭的時候當即執行一項任務,你須要等待他們工做的結果。你須要等待,由於在你能夠繼續以前,你須要他們的報告(paper)。

01 func waitForResult() {
02     ch := make(chan string)
03
04     go func() {
05         time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
06
07         ch <- "paper"
08
09         // 員工已經完成了,而且能夠自由地離開
10     }()
11
12     p := <-ch
13 }

在第2行中,建立了一個沒有緩衝的channel,該channel的屬性是 string 型數據將被髮送到信號。而後在第04行,一名僱員被僱傭,並當即被投入工做。當你在第04行僱傭了這名員工後,你會發現本身排在第12行,等待着這份報告。

一旦工做由第05行中的員工完成,他們就會在第07行經過有數據的channel發送結果給你。因爲這是一個沒有緩衝的通道,因此接收在發送以前就發生了,而且保證你已經收到告終果。一旦員工有了這樣的保證,他們就能夠自由地工做了。在這種狀況下,你不知道他們要花多長時間才能完成這項任務。

成本/效益

一個沒有緩衝的通道能夠保證接收到的信號被接收。這很好,但沒有什麼是免費的。這種擔保的成本是未知的延遲。在等待任務場景的過程當中,員工不知道要花多長時間才能發送那份報告。在等待結果的狀況下,你不知道須要多長時間才能讓員工發送結果。在這兩種狀況下,這種未知的延遲是咱們必需要面對的,由於須要保證。若是沒有這種保證行爲,邏輯是行不通的。

如下場景請你們結合以上內容,具體分析查看。

有數據信號 - 無保證 - 緩衝的 channel > 1

01 func fanOut() {
02     emps := 20
03     ch := make(chan string, emps)
04
05     for e := 0; e < emps; e++ {
06         go func() {
07             time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
08             ch <- "paper"
09         }()
10     }
11
12     for emps > 0 {
13         p := <-ch
14         fmt.Println(p)
15         emps--
16     }
17 }
01 func selectDrop() {
02     const cap = 5
03     ch := make(chan string, cap)
04
05     go func() {
06         for p := range ch {
07             fmt.Println("employee : received :", p)
08         }
09     }()
10
11     const work = 20
12     for w := 0; w < work; w++ {
13         select {
14             case ch <- "paper":
15                 fmt.Println("manager : send ack")
16             default:
17                 fmt.Println("manager : drop")
18         }
19     }
20
21     close(ch)
22 }

 有數據信號 - 延遲保證 - 緩衝channel 1 

01 func waitForTasks() {
02     ch := make(chan string, 1)
03
04     go func() {
05         for p := range ch {
06             fmt.Println("employee : working :", p)
07         }
08     }()
09
10     const work = 10
11     for w := 0; w < work; w++ {
12         ch <- "paper"
13     }
14
15     close(ch)
16 }

無數據信號 - 上下文(Context)

01 func withTimeout() {
02     duration := 50 * time.Millisecond
03
04     ctx, cancel := context.WithTimeout(context.Background(), duration)
05     defer cancel()
06
07     ch := make(chan string, 1)
08
09     go func() {
10         time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
11         ch <- "paper"
12     }()
13
14     select {
15     case p := <-ch:
16         fmt.Println("work complete", p)
17
18     case <-ctx.Done():
19         fmt.Println("moving on")
20     }
21 }

總結

在使用channel(或併發)時,關於保證、狀態和發送的信號的屬性很是重要。它們將幫助指導你實現你正在編寫的併發程序和算法所需的最佳行爲。它們將幫助你找到bug,並找出潛在的糟糕代碼。

在這篇文章中,咱們分享了一些示例程序,它們展現了在不一樣場景中信號的屬性是如何工做的。每一個規則都有例外,可是這些模式是開始的良好基礎。

相關文章
相關標籤/搜索