當談到併發時,許多編程語言都採用共享內存/狀態模型。然而,Go 通過實現 Communicating Sequential Processes(CSP)而與眾不同。在 CSP 中,程序由不共享狀態的並行處理器組成;相反,他們使用 Channel 來溝通和同步他們的行動。因此,對於有興趣採用 Go 的開發人員來説,理解 Channel 的工作原理變得至關重要。在本文中,我將使用地鼠經營他們想象中的咖啡館的令人愉快的類比來説明 Channel ,因為我堅信人類是更好的視覺學習者。
場景
Partier, Candier, Stringer 三人正在經營一家咖啡館。鑑於製作咖啡比接受訂單需要更多時間,Partier 將協助接受顧客的訂單,然後將這些訂單傳遞到廚房,由 Candier 和 Stringer 準備咖啡。
無緩衝 Channels
最初,咖啡館以最簡單的方式運營:每當收到新訂單時,Partier 都會將訂單放入 Channel 中,並等到 Candier 或 Stringer 接受後才接受任何新訂單。 Partier 和廚房之間的通信是通過使用 ch := make(chan Order) 創建的無緩衝 Channel 來實現的。當 Channel 中沒有掛單時,即使 Stringer 和 Candier 都準備好接受新訂單,它們也會保持空閒狀態並等待新訂單到達。
當收到新訂單時,Partier 將其放入 Channel 中,使 Candier 或 Stringer 可以接受該訂單。但是,在繼續接受新訂單之前,Partier 必須等待後廚的兩位工作者(Candier, Stringer)的其中一個從 Channel 中檢索並獲取訂單。
由於 Candier 和 Stringer 都可以接受新訂單,因此他們中的任何一個都會立即接受新訂單。但是,無法保證或預測收到訂單的具體收件人。 Stringer 和 Candier 之間的選擇是不確定的,它取決於調度和 Go 運行時的內部機制等因素。假設 Candier 收到了第一個訂單。
Candier 處理完第一個訂單後,又回到等待狀態。如果沒有新訂單到達,Candier 和 Stringer 這兩個工作人員將保持空閒狀態,直到 Partier 將另一個訂單放入通道中供他們處理。
當新訂單到達時,Stringer 和 Candier 都可以處理它。即使 Candier 剛剛處理了上一個訂單,接收新訂單的具體工人仍然是不確定的。在這種情況下,假設 Candier 再次被分配了第二個訂單。
新訂單 order3 到達,Candier 當前正忙於處理 order2,她沒有在隊列中等待 order := <-ch,Stringer 成為唯一可以接收 order3 的工作人員。因此,他會得到它。
在 order3 發送到 Stringer 後,order4 立即到達。此時,Stringer 和 Candier 都已忙於處理各自的訂單,沒有人可以接收 order4。由於 Channel 沒有緩衝,因此將 order4 放入其中會阻塞 Partier,直到 Stringer 或 Candier 可以接受 order4。這種情況值得特別注意,因為我經常看到人們對無緩衝通道(使用 make(chan order) 或 make(chan order, 0) 創建)和緩衝區大小為 1 的通道(使用 make(chan order, 1) 創建)感到困惑)。因此,他們錯誤地期望 ch <- order4 立即完成並接收 order5。如果您也是這麼想的,我在 Go Playground 上創建了一個片段來幫助您糾正您的誤解 https://go.dev/play/p/shRNiDDJYB4。
帶緩衝的 Channel
無緩衝通道可以工作,但是它限制了整體吞吐量。如果可以接受更多訂單並在後端(廚房)按順序處理它們,那就更好了。這可以通過緩衝通道來實現。現在,即使 Stringer 和 Candier 忙於處理訂單,Partier 仍然可以在通道中留下新訂單,並在通道未滿的情況下繼續接受其他訂單,例如最多 3 個掛單。
通過引入緩衝 Channel,咖啡館增強了處理更多訂單的能力。然而,仔細選擇適當的緩衝區大小以維持客户合理的等待時間至關重要。畢竟,沒有顧客願意忍受過長的等待。有時,拒絕新訂單可能比接受新訂單但無法及時履行訂單更容易接受。此外,在臨時容器化 (Docker) 應用程序中使用緩衝 Channel 時務必謹慎,因為容器會隨機重啓,在這種情況下從 Channel 恢復消息可能是一項具有挑戰性的任務,甚至近乎不可能。
Channels vs Blocking Queues
儘管本質上不同,Java 中的 Blocking Queue 用於線程之間的通信,而 Go 中的 Channel 用於 Goroutine 的通信,BlockingQueue 和 Channel 的行為有些相似。如果你熟悉BlockingQueue,理解 Channel 肯定會很容易。
常見使用場景
Channel 是 Go 應用程序中一項基本且廣泛使用的功能,可用於多種用途。Channel 的一些常見用例包括:
- Goroutine 通信:Channel 支持不同 Goroutine 之間的消息交換,允許它們進行協作,而無需直接共享狀態。
- 工作池:如上例所示,Channel 通常用於管理工作池,其中多個相同的工作者處理來自共享Channel 的傳入任務。
- 扇出、扇入:Channel 用於扇出、扇入模式,其中多個 Goroutine(扇出)執行工作並將結果發送到單個 Channel,而另一個 Goroutine(扇入)消耗這些結果。
- 超時和截止日期:Channel與 select 語句結合可用於處理超時和截止日期,確保程序可以優雅地處理延遲並避免無限期的等待。
我將在其他文章中更詳細地探討 Channel 的不同用法。然而,現在,讓我們通過實現上述咖啡館場景並見證渠道如何運作來結束這篇介紹性博客。我們將探討 Partier、Candier 和 Stringer 之間的交互,觀察 Channel 如何促進它們之間的順暢溝通和協調,從而實現咖啡館內高效的訂單處理和同步。
Show me your code!
package main
import (
"fmt"
"log"
"math/rand"
"sync"
"time"
)
func main() {
ch := make(chan order, 3)
wg := &sync.WaitGroup{} // More on WaitGroup another day
wg.Add(2)
go func() {
defer wg.Done()
worker("Candier", ch)
}()
go func() {
defer wg.Done()
worker("Stringer", ch)
}()
for i := 0; i < 10; i++ {
waitForOrders()
o := order(i)
log.Printf("Partier: I %v, I will pass it to the channel\n", o)
ch <- o
}
log.Println("No more orders, closing the channel to signify workers to stop")
close(ch)
log.Println("Wait for workers to gracefully stop")
wg.Wait()
log.Println("All done")
}
func waitForOrders() {
processingTime := time.Duration(rand.Intn(2)) * time.Second
time.Sleep(processingTime)
}
func worker(name string, ch <-chan order) {
for o := range ch {
log.Printf("%s: I got %v, I will process it\n", name, o)
processOrder(o)
log.Printf("%s: I completed %v, I'm ready to take a new order\n", name, o)
}
log.Printf("%s: I'm done\n", name)
}
func processOrder(_ order) {
processingTime := time.Duration(2+rand.Intn(2)) * time.Second
time.Sleep(processingTime)
}
type order int
func (o order) String() string {
return fmt.Sprintf("order-%02d", o)
}
您可以複製此代碼,對其進行調整並在 IDE 上運行它,以更好地瞭解通道的工作原理。
本文譯自:https://medium.com/stackademic/go-concurrency-visually-explained-channel-c6f88070aafa