看了这篇有关Go语言的Channel文章,整个人都感觉不好了

banq 16-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
}


这里状态变量bestScore 并不使用互斥锁进行保护,因为我们只有一个goroutine管理这个状态,新的分数是通过一个channel接受的。

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


现在开始一个启动game的构造器:

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


下一步,让我们假设有人给我们一个Player接口,这个接口可以获得分数score,也可能返回错误,因为也许网络连接错误或player已经退出。


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


下面是处理player,首先检查错误,然后将接受到的分数发给channel。

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


好了,我们有一个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()
}
}

这里只是引入了互斥锁,替代了原来的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


对于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修改过]

              

8