golang 的channels 行為

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

簡介

當我第一次使用 Go 的 channels 工作的時候,我犯了一個錯誤,把 channels 考慮為一個資料結構。我把 channels 看作為 goroutines 之間提供自動同步訪問的隊列。這種結構上的理解導致我寫了很多不好且結構複雜的並發代碼。

隨著時間的推移,我認識到最好的方式是忘記 channels 是資料結構,轉而關注它的行為。所以現在談論到 channels,我只考慮一件事情:signaling(訊號)。一個 channel 允許一個 goroutine 給另外一個發特定事件的訊號。訊號是使用 channel 做一切事情的核心。將 channel 看作是一種訊號機制,可以讓你寫出明確定義和精確行為的更好代碼。

為了理解訊號怎樣工作,我們必須理解以下三個特性:

  • 交付保證
  • 狀態
  • 有資料或無資料

這三個特性共同構成了圍繞訊號的設計哲學,在討論這些特性之後,我將提供一系列程式碼範例,這些樣本將示範使用這些屬性的訊號。

交付保證

交付保證基於一個問題:“我是否需要保證由特定的 goroutine 發送的訊號已經被接收?”

換句話說,我們可以給出清單1的樣本:

清單1

01 go func() {02     p := <-ch // Receive03 }()0405 ch <- "paper" // Send

發送的 goroutine 是否需要保證在第五行中發送給 channel 的 paper,在繼續執行前, 會被第二行的 goroutine 接收。

基於這個問題的答案,你將知道使用兩種類型的 channels 中的哪種:無緩衝有緩衝。每個channel圍繞交付保證提供不同的行為。

圖1

保證很重要,並且如果你不這樣認為,我有很多東西兜售給你。當然,我想開個玩笑,當你的生活沒有保障的時候你不會害怕嗎?在編寫並發代碼時,對是否需要一項保證有很強的理解是至關重要的。隨著繼續,你將學會如何做決策。

狀態

一個 channel 的行為直接被它當前的狀態所影響。一個channel 的狀態是:nilopenclosed

下面的清單2展示了怎樣聲明或把一個 channel放進這三個狀態。

清單2

// ** nil channel// A channel is in a nil state when it is declared to its zero valuevar ch chan string// A channel can be placed in a nil state by explicitly setting it to nil.ch = nil// ** open channel// A channel is in a open state when it’s made using the built-in function make.ch := make(chan string)    // ** closed channel// A channel is in a closed state when it’s closed using the built-in function close.close(ch)

狀態決定了怎樣send(發送)和receive(接收)操作行為。

訊號通過一個 channel 發送和接收。不要說讀和寫,因為 channels 不執行 I/O。

圖2

當一個 channel 是 nil 狀態,任何試圖在 channel 的發送或接收都將會被阻塞。當一個 channel 是在 open 狀態,訊號可以被發送和接收。當一個 channel 被置為 closed 狀態,訊號將不在被發送,但是依然可以接收訊號。

這些狀態將在你遭遇不同的情況的時候可以提供不同的行為。當結合狀態交付保證,作為你設計選擇的結果,你可以分析你承擔的成本/收益。你也可以僅僅通過讀代碼快速發現錯誤,因為你懂得 channel 將表現出什麼行為。

有資料和無資料

最後的訊號特性需要考慮你是否需要訊號有資料或者無資料。

在一個 channel 中有資料的訊號被執行一個發送。

清單3

01 ch <- "paper"

當你的訊號有資料,它通常是因為:

  • 一個 goroutine 被要求啟動一個新的 task。
  • 一個 goroutine 傳達一個結果。

無資料訊號通過關閉一個 channel。

清單4

01 close(ch)

當訊號沒有資料的時候,它通常是因為:

  • 一個 goroutine 被告知停止它正在做的事情。
  • 一個 goroutine 報告它們已經完成,沒有結果。
  • 一個 goroutine 報告它已經完成處理並且關閉。

這些規則也有例外,但這些都是主要的用例,並且我們將在本文中重點討論這些問題。我認為這些規則例外的情況是最初的代碼味道。

無資料訊號的一個好處是一個單獨的 goroutine 可以立刻給很多 goroutines 訊號。有資料的訊號通常是在 goroutines 之間一對一的交換資料。

有資料訊號

當你使用有資料訊號的時候,依賴於你需要保證的類型,有三個channel配置選項可以選擇。

圖3:有資料訊號

這三個 channel 選項是:Unbuffered, Buffered >1Buffered =1

  • 有保證

    • 一個無緩衝的channel給你保證被發送的訊號已經被接收。

      • 因為訊號接收發生在訊號發送完成之前。
  • 無保證

    • 一個 size > 1 的有緩衝的 channel 不會保證發送的訊號已經被接收。

      • 因為訊號發送發生在訊號接送完成之前。
  • 延遲保證

    • 一個 size = 1 的有緩衝 channel 提供延遲保證。它可以保證先前發送的訊號已經被接收。

      • 因為第一個接收訊號,發生在第二個完成的發送訊號之前。

緩衝大小絕對不能是一個隨機數字,它必須是為一些定義好的約束而計算出來的。在計算中沒有無窮大,無論是空間還是時間,所有的東西都必須要有良好的定義約束。

無資料訊號

無資料訊號主要用於取消,它允許一個 goroutine 發送訊號給另外一個來取消它們正在做的事情。取消可以被有緩衝和無緩衝的channels實現,但是在沒有資料發送的情況下使用緩衝 channel 會更好。

圖4:無資料訊號

內建的函數 close 被用於無資料訊號。正如上面狀態章節所解釋的那樣,你依然可以在channel關閉的時候接收訊號。實際上,在一個關閉的channel上的任何接收都不會被阻塞,並且接收操作將一直返回。

在大多數情況下,你想使用標準的庫 context 包來實現無資料訊號。context 包使用一個無緩衝channel傳遞訊號以及內建函數close發送無資料訊號。

如果你選擇使用你自己的 channel 而不是 context包來取消,你的channel 應該是chan struct{} 類型,這是一種零空間的慣用方式,用來表示一個訊號僅僅用於訊號傳遞。

情境

有了這些特性,更進一步理解它們在實踐中怎樣工作的最好方式就是運行一系列的代碼情境。當我在讀寫 channel 基礎代碼的時候,我喜歡把goroutines想像成人。這個形象對我非常有協助,我將把它用作下面的協助工具輔助。

有資料訊號 - 保證 - 無緩衝 Channels

當你需要知道一個被發送的訊號已經被接收的時候,有兩種情況需要考慮。它們是 等待任務等待結果

情境1 - 等待任務

考慮一下作為一名經理,需要僱傭一名新員工。在本情境中,你想你的新員工執行一個任務,但是他們需要等待直到你準備好。這是因為在他們開始前你需要遞給他們一份報告。

清單5

線上示範地址

01 func waitForTask() {02     ch := make(chan string)0304     go func() {05         p := <-ch0607         // Employee performs work here.0809         // Employee is done and free to go.10     }()1112     time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)1314     ch <- "paper"15 }

在清單5的第2行,一個帶有屬性的無緩衝channel被建立,string 資料將與訊號一起被發送。在第4行,一名員工被僱傭並在開始工作前,被告訴等待你的訊號【在第5行】。第5行是一個 channel 接收,引起員工阻塞直到等到你發送的報告。一旦報告被員工接收,員工將執行工作並在完成的時候可以離開。

你作為經理正在並發的與你的員工工作。因此在第4行你僱傭員工之後,你發現你自己需要做什麼來解鎖並且發訊號給員工(第12行)。值得注意的是,不知道要花費多長的時間來準備這份報告(paper)。

最終你準備好給員工發訊號,在第14行,你執行一個有資料訊號,資料就是那份報告。由於一個無緩衝的channel被使用,你得到一個保證就是一旦你操作完成,員工就已經接收到了這份報告。接收發生在發送之前。

技術上你所知道的一切就是在你的channel發送操作完成的同時員工接收到了這份報告。在兩個channel操作之後,調度器可以選擇執行它想要執行的任何語句。下一行被執行的代碼是被你還是員工是不確定的。這意味著使用print語句會欺騙你關於事件的執行順序。

情境2 - 等待結果

在下一個情境中,事情是相反的。這時你想你的員工一被僱傭就立即執行他們的任務。然後你需要等待他們工作的結果。你需要等待是因為在你繼續前你需要他們發來的報告。

清單6
線上示範地址

01 func waitForResult() {02     ch := make(chan string)0304     go func() {05         time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)0607         ch <- "paper"0809         // Employee is done and free to go.10     }()1112     p := <-ch13 }

成本/收益

無緩衝 channel 提供了訊號被發送就會被接收的保證,這很好,但是沒有任何東西是沒有代價的。這個成本就是保證是未知的延遲。在等待任務情境中,員工不知道你要花費多長時間發送你的報告。在等待結果情境中,你不知道員工會花費多長時間把報告發送給你。

在以上兩個情境中,未知的延遲是我們必須面對的,因為它需要保證。沒有這種保證行為,邏輯就不會起作用。

有資料訊號 - 無保證 - 緩衝 Channels > 1

情境1 - 扇出(Fan Out)

扇出模式允許你拋出明確定義數量的員工在同時工作的問題上。由於你每個任務都有一個員工,你很明確的知道你會接收多少個報告。你可能需要確保你的盒子有適量的空間來接收所有的報告。這就是你員工的收益,不需要等待你來提交他們的報告。但是他們確實需要輪流把報告放進你的盒子,如果他們幾乎同一時間到達盒子。

再次假設你是經理,但是這次你僱傭一個團隊的員工,你有一個單獨的任務,你想每個員工都執行它。作為每個單獨的員工完成他們的任務,他們需要給你提供一張報告放進你桌子上的盒子裡面。

清單7
示範地址

01 func fanOut() {02     emps := 2003     ch := make(chan string, emps)0405     for e := 0; e < emps; e++ {06         go func() {07             time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)08             ch <- "paper"09         }()10     }1112     for emps > 0 {13         p := <-ch14         fmt.Println(p)15         emps--16     }17 }

在清單7的第3行,一個帶有屬性的有緩衝channel被建立,string 資料將與訊號一起被發送。這時,由於在第2行聲明的 emps 變數,將建立有 20個緩衝的 channel。

在第5行和第10行之間,20 個員工被僱傭,並且他們立即開始工作。在第7行你不知道每個員工將花費多長時間。這時在第8行,員工發送他們的報告,但這一次發送不會阻塞等待接收。因為在盒子裡為每位員工準備的空間,在 channel 上的發送僅僅與其他在同一時間想發送他們報告的員工競爭。

在 12 行和16行之間的代碼全部是你的操作。在這裡你等待20個員工來完成他們的工作並且發送報告。在12行,你在一個迴圈中,在 13 行你被阻塞在一個 channel 等待接收你的報告。一旦報告接收完成,報告在14被列印,並且本地的計數器變數被消耗來表明一個員工意見完成了他的工作。

情境2 - Drop

Drop模式允許你在你的員工在滿負荷的時候丟掉工作。這有利於繼續接受用戶端的工作,並且從不施加壓力或者是這項工作可接受的延遲。這裡的關鍵是知道你什麼時候是滿負荷的,因此你不承擔或過度承諾你將嘗試完成的工作量。通常整合測試或度量可以協助你確定這個數字。

假設你是經理,你僱傭了單個員工來完成工作。你有一個單獨的任務想員工去執行。當員工完成他們任務時,你不在乎知道他們已經完成了。最重要的是你能或不能把新工作放入盒子。如果你不能執行發送,這時你知道你的盒子滿了並且員工是滿負荷的。這時候,新工作需要丟棄以便讓事情繼續進行。

清單8
示範地址

01 func selectDrop() {02     const cap = 503     ch := make(chan string, cap)0405     go func() {06         for p := range ch {07             fmt.Println("employee : received :", p)08         }09     }()1011     const work = 2012     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     }2021     close(ch)22 }

在清單8的第3行,一個有屬性的有緩衝 channel 被建立,string 資料將與訊號一起被發送。由於在第2行聲明的cap 常量,這時建立了有5個緩衝的 channel。

從第5行到第9行,一個單獨的員工被僱傭來處理工作,一個 for range被用於迴圈處理 channel 的接收。每次一份報告被接收,在第7行被處理。

在第11行和19行之間,你嘗試發送20分報告給你的員工。這時一個 select語句在第14行的第一個case被用於執行發送。因為default從句被用於第16行的select語句。如果發送被堵塞,是因為緩衝中沒有多餘的空間,通過執行第17行發送被丟棄。

最後在第21行,內建函數close被調用來關閉channel。這將發送沒有資料的訊號給員工表明他們已經完成,並且一旦他們完成指派給他們的工作可以立即離開。

成本/收益

有緩衝的 channel 緩衝大於1提供無保證發送的訊號被接收到。離開保證是有好處的,在兩個goroutine之間通訊可以降低或者是沒有延遲。在扇出情境,這有一個有緩衝的空間用於存放員工將被發送的報告。在Drop情境,緩衝是測量能力的,如果容量滿,工作被丟棄以便工作繼續。

在兩個選擇中,這種缺乏保證是我們必須面對的,因為延遲降低非常重要。0到最小延遲的要求不會給系統的整體邏輯造成問題。

有資料訊號 - 延遲保證- 緩衝1的channel

情境1 - 等待任務

清單9
示範地址

01 func waitForTasks() {02     ch := make(chan string, 1)0304     go func() {05         for p := range ch {06             fmt.Println("employee : working :", p)07         }08     }()0910     const work = 1011     for w := 0; w < work; w++ {12         ch <- "paper"13     }1415     close(ch)16 }

在清單9的第2行,一個帶有屬性的一個緩衝大小的 channel 被建立,string 資料將與訊號一起被發送。在第4行和第8行之間,一個員工被僱傭來處理工作。for range被用於迴圈處理 channel 的接收。在第6行每次一份報告被接收就被處理。

在第10行和13行之間,你開始發送你的任務給員工。如果你的員工可以跑的和你發送的一樣快,你們之間的延遲會降低。但是每次發送你成功執行,你需要保證你提交的最後一份工作正在被進行。

在最後的第15行,內建函數close 被調用關閉channel,這將會發送無資料訊號給員工告知他們工作已經完成,可以離開了。儘管如此,你提交的最後一份工作將在 for range中斷前被接收。

無資料訊號 - Context

在最後這個情境中,你將看到從 Context 包中使用 Context 值怎樣取消一個正在啟動並執行goroutine。這所有的工作是通過改變一個已經關閉的無緩衝channel來執行一個無資料訊號。

最後一次你是經理,你僱傭了一個單獨的員工來完成工作,這次你不會等待員工未知的時間完成他的工作。你分配了一個截止時間,如果你的員工沒有按時完成工作,你將不會等待。

清單10
示範地址

01 func withTimeout() {02     duration := 50 * time.Millisecond0304     ctx, cancel := context.WithTimeout(context.Background(), duration)05     defer cancel()0607     ch := make(chan string, 1)0809     go func() {10         time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)11         ch <- "paper"12     }()1314     select {15     case p := <-ch:16         fmt.Println("work complete", p)1718     case <-ctx.Done():19         fmt.Println("moving on")20     }21 }

在清單10的第2行,一個時間值被聲明,它代表了員工將花費多長時間完成他們的工作。這個值被用在第4行來建立一個50毫秒逾時的 context.Context 值。context 包的 WithTimeout 函數返回一個 Context 值和一個取消函數。

context包建立一個goroutine,一旦時間值到期,將關閉與Context 值關聯的無緩衝channels。不管事情如何發生,你需要負責調用cancel 函數。這將清理被Context建立的東西。cancel被調用不止一次是可以的。

在第5行,一旦函數中斷,cancel函數被 deferred 執行。在第7行,1個緩衝的channels被建立,它被用於被員工發送他們工作的結果給你。在第09行和12行,員工被僱傭兵立即投入工作,你不需要指定員工花費多長時間完成他們的工作。

在第14行和20行之間,你使用 select 語句來在兩個channels接收。在第15行的接收,你等待員工發送他們的結果。在第18行的接收,你等待看context 包是否正在發送訊號50毫秒的時間到了。無論你首先收到哪個訊號,都將有一個被處理。

這個演算法的一個重要方面是使用一個緩衝的channels。如果員工沒有按時完成,你將離開而不會給員工任何通知。對於員工而言,在第11行他將一直發送他的報告,你在或者不在那裡接收,他都是盲目的。如果你使用一個無緩衝channels,如果你離開,員工將一直阻塞在那嘗試你給發送報告。這會引起goroutine泄漏。因此一個緩衝的channels用來防止這個問題發生。

總結

當使用 channels(或並發) 時,在保證,channel狀態和發送過程中訊號屬性是非常重要的。它們將協助你實現你並發程式需要的更好的行為以及你寫的演算法。它們將協助你找出bug和聞出潛在的壞代碼。

在本文中,我分享了一些程式樣本來展示訊號屬性工作在不同的情境中。凡事都有例外,但是這些模式是非常良好的開端。

作為總結回顧下這些要點,何時,如何有效地思考和使用channels:

語言機制

  • 使用 channels 來編排和協作 goroutines:

    • 關注訊號屬性而不是資料共用
    • 有資料訊號和無資料訊號
    • 詢問它們用於同步訪問共用資料的用途

      • 有些情況下,對於這個問題,通道可以更簡單一些,但是最初的問題是。
  • 無緩衝 channels:

    • 接收發生在發送之前
    • 收益:100%保證訊號被接收
    • 成本:未知的延遲,不知道訊號什麼時候將被接收。
  • 有緩衝 channels:

    • 發送發生在接收之前。
    • 收益:降低訊號之間的阻塞延遲。
    • 成本:不保證訊號什麼時候被接收。

      • 緩衝越大,保證越少。
      • 緩衝為1可以給你一個延遲發送保證。
  • 關閉的 channels:

    • 關閉發生在接收之前(像緩衝)。
    • 無資料訊號。
    • 完美的訊號取消或截止。
  • nil channels:

    • 發送和接收都阻塞。
    • 關閉訊號。
    • 完美的速度限制或短時停工。

設計哲學

  • 如果在 channel上任何給定的發送能引起發送 goroutine 阻塞:

    • 不允許使用大於1的緩衝channels。

      • 緩衝大於1必須有原因/測量。
    • 必須知道當發送 goroutine阻塞的時候發生了什麼。
  • 如果在 channel 上任何給定的發送不會引起發送阻塞:

    • 每個發送必須有確切的緩衝數字。

      • 扇出模式。
    • 有緩衝測量最大的容量。

      • Drop 模式。
  • 對於緩衝而言,少即是多。

    • 當考慮緩衝的時候,不要考慮效能。
    • 緩衝可以協助降低訊號之間的阻塞延遲。

      • 降低阻塞延遲到0並不一定意味著更好的輸送量。
      • 如果一個緩衝可以給你足夠的輸送量,那就保持它。
      • 緩衝大於1的問題需要測量大小。
      • 儘可能找到提供足夠輸送量的最小緩衝
相關文章

聯繫我們

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