Golang channel 的本質: Channels orchestrate; mutexes serialize

channel 是 Go 語言獨有的一個特性,相比 goroutine 更加抽象,也更加難以理解。畢竟後者能夠類比線程、進程。《Go channels are bad and you should feel bad》 說起在使用 channel 和 mutex 時的困惑。其中提到過一個簡單的程序,能夠保存一場遊戲的各個選手中的最高分。做者分別使用 channelmutex 來實現該功能。html

channel 版

首先定義 Game 結構體:git

type Game struct {
  bestScore int
  scores    chan int
}

bestScore 不會使用 mutex 保護,而是使用一個獨立的 goroutine 從 channel 接收數據,而後更新其狀態。github

func (g *Game) run() {
  for score := range g.scores {
    if g.bestScore < score {
      g.bestScore = score
    }
  }
}

而後定義構造函數來開始一場遊戲web

func NewGame() (g *Game) {
  g = &Game{
    bestScore: 0,
    scores:    make(chan int),
  }
  go g.run()
  return g
}

緊接着,定義 Player 接口返回該選手的分數,同時返回 error 用以表示 選手放棄比賽等異常狀況。編程

type Player interface {
  NextScore() (score int, err error)
}

遊戲經過 channel 接收全部選手的分數segmentfault

func (g *Game) HandlePlayer(p Player) error {
  for {
    score, err := p.NextScore()
    if err != nil {
      return err
    }
    g.scores <- score
  }
}

最終,Game 得以實現線程安全的記錄選手的最高分,一切都很完美。安全

該實現大爲成功,遊戲服務同時建立了不少的遊戲。不久,你發現有選手偶爾會中止遊戲,不少遊戲也再也不有選手玩了,可是卻沒有什麼機制中止遊戲循環。你正被廢棄的 (*Game).run goroutine 壓垮。網絡

mutex 版

然而,請注意使用 mutex 的解決方案的簡單性,它甚至不存在以上問題:併發

type Game struct {
  mtx sync.Mutex
  bestScore int
}

func NewGame() *Game {
  return &Game{}
}

func (g *Game) HandlePlayer(p Player) error {
  for {
    score, err := p.NextScore()
    if err != nil {
      return err
    }
    g.mtx.Lock()
    if g.bestScore < score {
      g.bestScore = score
    }
    g.mtx.Unlock()
  }
}

channel 用以編排,mutex 用以串行

若是是你來實現,你更願意使用 channel 仍是 mutex
按照目前提供的信息,毫無疑問,我會選擇後者。app

那 channel 和 mutex 有什麼區別呢?在什麼場景下該使用 channel ?

其實 Rob PikeGo Proverbs 中總結爲:

Channels orchestrate; mutexes serialize.

翻譯就是

channel 用以編排,mutex 用以串行

此句話很簡單,但也很抽象。究竟該怎樣理解呢?

channel vs mutex

Rob Pike 在講述《Concurrency is not Parallelism》中開篇,即提到:

  1. 世界萬物是並行的,可是當前的編程語言倒是面向對象的
  2. Golang 但願經過 goroutine(併發執行)、channel(同步和數據傳遞)、select(多路併發控制)來實現並行

在以前的文章中,我提到過

對於其餘語言的使用者,對於他們而言,程序中的流程控制通常意味着:

  • if/else
  • for loop

在 Go 中,相似的理解僅僅對了一小半。由於 channel 和 select 纔是流程控制的重點。
channel 提供了強大能力,幫助數據從一個 goroutine 流轉到另外一個 goroutine。也意味着,channel 對程序的 數據流控制流 同時存在影響。

channel 只是 Go 語言並行化工具集的一部分,其同時肩負了 數據流控制流 的職責,它是程序結構的組織者。對比來看,mutex 則只關注數據,保障數據串行訪問

編排

再談 channel 的編排,能夠看下 《Go Concurrency Patterns》中搜索舉例:

/*
Example: Google Search 3.0
Given a query, return a page of search results (and some ads).
Send the query to web search, image search, YouTube, Maps, News, etc. then mix the results.
*/
c := make(chan Result)
go func() { c <- First(query, Web1, Web2) } ()
go func() { c <- First(query, Image1, Image2) } ()
go func() { c <- First(query, Video1, Video2) } ()
timeout := time.After(80 * time.Millisecond)
for i := 0; i < 3; i++ {
    select {
    case result := <-c:
        results = append(results, result)
    case <-timeout:
        fmt.Println("timed out")
        return
    }
}

不管程序執行在幾個核心的機器上,程序的並行結構都沒有任何變化,以下:

orchestrate.png

講到程序結構的編排,能夠跟服務編排的 Kubernetes 類比。 若是說 goroutine 是 K8S 的容器,channel 就是 K8S 的網絡(如,overlay)。Kubernetes 使用戶可以以任何規模部署和擴展其微服務應用程序,Golang 使程序可以在任何數量 CPU 的機器上執行和和擴展進行充分的並行。

總結

就像《Concurrency is not Parallelism》說明的那樣,目前 channel很大程度的被誤用或濫用了。瞭解清楚 channel 的本質,才能使用正確的工具作對的事。

Goroutines and channels are big ideas. They're tools for program construction.
But sometimes all you need is a reference counter.
Go has "sync" and "sync/atomic" packages that provide mutexes, condition variables, etc. They provide tools for smaller problems.
Often, these things will work together to solve a bigger problem.
Always use the right tool for the job.

本文涉及源代碼go-test: 《go-channel-vs-mutex》

本文做者:cyningsun
本文地址https://www.cyningsun.com/05-...
版權聲明:本博客全部文章除特別聲明外,均採用 CC BY-NC-ND 3.0 CN 許可協議。轉載請註明出處!

相關文章
相關標籤/搜索