诠释 Channels orchestrate; mutexes serialize

988 阅读2分钟

channel 是 Go 语言独有的一个特性,相比 goroutine 更加抽象,也更加难以理解。毕竟后者可以类比线程、进程。《Go channels are bad and you should feel bad》 提及在使用 channel 和 mutex 时的困惑。其中提到过一个简单的程序,可以保存一场游戏的各个选手中的最高分。作者分别使用 channelmutex 来实现该功能。

channel 版

首先定义 Game 结构体:

type Game struct {
  bestScore int
  scores    chan int
}

bestScore 不会使用 mutex 保护,而是使用一个独立的 goroutine 从 channel 接收数据,然后更新其状态。

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

然后定义构造函数来开始一场游戏

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 接收所有选手的分数

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
按照目前提供的信息,毫无疑问,我会选择后者。

那 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
本文地址www.cyningsun.com/05-15-2021/…
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-ND 3.0 CN 许可协议。转载请注明出处!