看了這篇有關Go語言的Channel文章,整個人都感覺不好了

banq發表於2016-03-03
Go的Channel是一個很強大的併發資料模型,在一個傳送者和多個消費者情況下工作得最好,但是如果是多個傳送者,那麼在Channel關閉時需要協調多個傳送者,等待它們傳送消費完畢,同時也會導致一個Channel多次關閉的情況,這個問題Go社群已經注意到,並正在試圖解決:issue#14601

讓我們看看來自

go channels are bad and you should feel bad | jtol一文是如何抱怨Channel有多不好,以下是原文大意轉述:

首先,不用質疑的是,該文博主自從2010中期就開始使用Go語言,參與編寫了425K行的純Go程式碼,併為Go社群提供了不少開源專案。

Go語言是以Channel和Goroutine(輕量執行緒)著稱,它的理論模型主要是基於CSP模型(Communicating Sequential Processes),但是該文博主認為,從程式設計師角度看,Go的Channel模型實現方式是錯誤的,它是一個徹底的反模式。

CSP模型是一種高併發計算模型,這種模型本身是試圖透過一個通道同步管理傳送和接受,但是,如果一旦你引入其他同步機制如互斥鎖mutex、semaphore或條件判斷變數,你就再也不是純的CSP了。

下面讓我們看看使用GO編寫CSP模型,這個案例是關於如何接受到選手的最高分。

首先,使用Game作為資料結構:

type Game struct {
  bestScore int
  scores    chan int
}
<p class="indent">


這裡狀態變數bestScore 並不使用互斥鎖進行保護,因為我們只有一個goroutine管理這個狀態,新的分數是透過一個channel接受的。

func (g *Game) run() {
  for score := range g.scores {
    if g.bestScore < score {
      g.bestScore = score
    }
  }
}
<p class="indent">


現在開始一個啟動game的構造器:

func NewGame() (g *Game) {
  g = &Game{
    bestScore: 0,
    scores:    make(chan int),
  }
  go g.run()
  return g
}
<p class="indent">


下一步,讓我們假設有人給我們一個Player介面,這個介面可以獲得分數score,也可能返回錯誤,因為也許網路連線錯誤或player已經退出。

type Player interface {
  NextScore() (score int, err error)
}
<p class="indent">


下面是處理player,首先檢查錯誤,然後將接受到的分數發給channel。

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


好了,我們有一個Game型別,能夠以執行緒安全方式跟蹤Player的最高分。

當你將這個遊戲上線執行後,你獲得了非常地成功,你的遊戲伺服器上執行了很多這樣的Game程式。但是你會發現人們有時會離開你的遊戲,許多遊戲裡面沒有任何玩家,但是遊戲本身沒有停止而是不停地在迴圈執行,你被(*Game).run協程搞得不知所措了。

其實這個案例完全可以不使用Channel簡單地完成:

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()
  }
}
<p class="indent">

這裡只是引入了互斥鎖,替代了原來的Channel方式。使用傳統的同步鎖替代CSP的Go channel居然解決了問題。

那麼,讓我們看看Go語言的Channel背後是什麼?Channel是使用鎖保證連續訪問的執行緒安全性,使用Channel同步化記憶體的訪問,其實你是在使用鎖(banq注:這違背了CSP模型本身定義,CSP存在的前提就是避免使用鎖。),鎖被執行緒安全的佇列給包裝了,那麼Go的這個神秘的鎖與直接使用標準庫中mutex有什麼區別?下面是兩者效能測試結果:

BenchmarkSimpleSet-8 3000000 391 ns/op
BenchmarkSimpleChannelSet-8 1000000 1699 ns/op
<p class="indent">


對於unbuffered channel也是同樣測試結果。(測試結果表明:Channel效能比mutex效能要差得多)

也許Go的排程器會提高,但是這意味著老的舊的互斥鎖和條件變數表現得非常好,更有效,更快速。如果你需要效能,那麼就使用這些真正的方法(互斥鎖等方法)。

此後,該文作者還指出Channel其實並不能和其他併發原始模型很好地組合在一起。比如Channel和互斥鎖放在一起就帶來不確定性。

他還指出,在API中使用Channel反而使得問題變得糟糕,你完全可以在沒有任何協程情況下使用互斥鎖mutexe, semaphores,和回撥callbacks編寫API,有時,回撥函式反而更加強大,雖然,goroutine協程口口聲聲說是用來替代萬惡的回撥函式的。協程只是比執行緒較為輕量,但是較為輕量不代表它是最輕量的。

他還談到:對一個已經關閉的Channel再進行關閉或傳送操作是非常痛苦,如果你要關閉一個Channel,需要將關閉狀態進行同步化操作,使用mutex互斥鎖等,但是它們又不能很好地協調在一起,為什需要有關閉的狀態?因為這樣才會防止其他寫入者同時寫入或關閉一個已經關閉的channel。

最後,該文博主還未未來Go 2.0提出了更多建設性意見。

原文:

go channels are bad and you should feel bad | jtol

Actor模型和CSP模型的區別

[該貼被admin於2016-03-07 10:17修改過]

相關文章