PART 1
This commit is contained in:
Xingyu Wang 2021-01-05 00:54:09 +08:00
parent af9570ea9d
commit 995624f5be

View File

@ -7,38 +7,35 @@
[#]: via: (https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-should-feel-bad)
[#]: author: (jtolio.com https://www.jtolio.com/)
Go 通道不好,你也应该感到不好受
Go 通道不好,你应该感觉很难过
======
_更新如果你是从一篇题为 《Go is not good》 的概要文章来看这篇博文的话,那么我想表明的是,我很惭愧自己被列在这样的名单上。 Go 绝对是我使用过的最不糟糕的的编程语言。在我写作本文时,我想遏制我所看到的一种趋势,那就是过度使用 Go 的一些较复杂的部分。我仍然认为 <ruby>
通道<rt>Channels</rt>
</ruby>可以更好但是总体而言Go 很棒。这就像你最喜欢的工具箱中包含 [这个][1] 一样这个工具可用即使可能有更多用途而且它仍然可以是你最喜欢的工具箱_
_更新 2如果我没有指出这项对真实问题的优秀调查那我将是失职的[理解 Go 中的实际并发错误][2]。这项调查的一个重要发现是...Go 通道会导致很多错误。_
> 更新:如果你是从一篇题为 《糟糕的 Go 语言》 的汇编文章看到这篇博文的话那么我想表明的是我很惭愧被列在这样的名单上。Go 绝对是我使用过的最不糟糕的的编程语言。在我写作本文时,我是想遏制我所看到的一种趋势,那就是过度使用 Go 的一些较复杂的部分。我仍然认为 <ruby>
通道<rt>Channel</rt></ruby0>可以更好但是总体而言Go 很棒。这就像你最喜欢的工具箱中有 [这个工具][1];它可以有用途(甚至还可能有更多的用途),它仍然可以成为你最喜欢的工具箱!
>
> 更新 2如果我没有指出这项对真实问题的优秀调查那我将是失职的《[理解 Go 中的实际并发错误][2]》。这项调查的一个重要发现是...Go 通道会导致很多错误。
从 2010 年中后期开始,我就断断续续地在使用 Google 的 [Go 编程语言][3],自 2012 年 1 月开始(在 Go 1.0 之前!),我就用 Go 为 [Space Monkey][4] 编写了合法的产品代码。我对 Go 的最初体验可以追溯到我在研究 Hoare 的 [通信顺序进程][5] 并发模型和 [Matt Might][7] 的 [UCombinator 研究组][8] 下的 [π-演算][6] 时,作为我([现在已重定向][9]博士工作的一部分以更好地支持多核开发。Go 就是在那时发布的(多么巧合啊!),我当即就开始学习尝试了。
它很快就成为了 Space Monkey 开发的核心部分。目前,我们在 Space Monkey 的生产系统有超过 42.5 万行的纯 Go 代码_不_ 包括我们所有的 vendored 库中的代码量,这将使它接近 150 万行),所以也并不是你见过的最多的 Go 代码,但是对于相对年轻的语言,我们是重度用户。我们之前 [写了我们的 Go 使用情况][10]。也开源了一些使用率很高的库;许多人似乎是我们的 [OpenSSL 绑定][11](比 [crypto/tls][12] 更快,但请保持 openssl 本身是最新的!)、我们的 [错误处理库][13]、[日志库][14] 和 [度量标准收集库/zipkin 客户端][15] 的粉丝。我们使用 Go、我们热爱 Go、我们认为它是目前为止我们使用过的最不糟糕的、符合我们需求的编程语言。
它很快就成为了 Space Monkey 开发的核心部分。目前,我们在 Space Monkey 的生产系统有超过 42.5 万行的纯 Go 代码_不_ 包括我们所有的 vendored 库中的代码量,这将使它接近 150 万行),所以也并不是你见过的最多的 Go 代码,但是对于相对年轻的语言,我们是重度用户。我们之前 [写了我们的 Go 用法][10]。也开源了一些使用率很高的库;许多人似乎是我们的 [OpenSSL 绑定][11](比 [crypto/tls][12] 更快,但请保持 openssl 本身是最新的!)、我们的 [错误处理库][13]、[日志库][14] 和 [度量标准收集库/zipkin 客户端][15] 的粉丝。我们使用 Go、我们热爱 Go、我们认为它是目前为止我们使用过的最不糟糕的、符合我们需求的编程语言。
尽管我也不认为我能说服自己不要提及我的广泛避免 [goroutine-local-storage 库][16] (尽管它是一个你不应该使用的 hack但它是一个漂亮的 hack),希望我的其他经历足以证明我在解释我故意煽动性的帖子标题之前知道我在说什么。
尽管我也不认为我能说服自己不要提及我的广泛避免使用 [goroutine-local-storage 库][16] (尽管它是一个你不应该使用的魔改技巧,但它是一个漂亮的魔改),希望我的其他经历足以证明我在解释我故意煽动性的帖子标题之前知道我在说什么。
![][17]
### 等等,什么?
如果你在大街上问一个有名的程序员Go 有什么特别之处? 她很可能会告诉你 Go 最出名的是<ruby>通道<rt>Channels</rt></ruby> 和 goroutine。 Go 的理论基础很大程度上是建立在 Hoare 的 CSP (<ruby>通信顺序进程<rt>Communicating Sequential Processes</rt></ruby>) 模型上的,该模型本身令人着迷且有趣,我坚信,到目前为止,我们产生的收益远远超过了我们的预期。
如果你在大街上问一个有名的程序员Go 有什么特别之处? 她很可能会告诉你 Go 最出名的是<ruby>通道<rt>Channels</rt></ruby> 和 goroutine。 Go 的理论基础很大程度上是建立在 Hoare 的 CSP<ruby>通信顺序进程<rt>Communicating Sequential Processes</rt></ruby>)模型上的,该模型本身令人着迷且有趣,我坚信,到目前为止,它产生的收益远远超过了我们的预期。
CSP和 π-演算)都使用通信作为核心同步原语,因此 Go 会有通道是有道理的。Rob Pike 对 CSP 着迷(有充分的理由)[相当深][18] 已经有一段时间了。([当时][19] 和 [现在][20])
CSP和 π-演算)都使用通信作为核心同步原语,因此 Go 会有通道是有道理的。Rob Pike 对 CSP 着迷(有充分的理由)[相当深][18] 已经有一段时间了。[当时][19] 和 [现在][20]
但是从务实的角度来看(也是 Go 引以为豪的Go 把通道搞错了。在这一点上,实现的通道在我的书中几乎是一个坚实的反模式。为什么?亲爱的读者,让我细数其中的方法。
但是从务实的角度来看(也是 Go 引以为豪的Go 把通道搞错了。在这一点上,通道的实现在我的书中几乎是一个坚实的反模式。为什么这么说呢?亲爱的读者,让我细数其中的方法。
#### 你可能最终不会只使用通道
#### 你可能最终不会只使用通道
Hoare 的 “通信顺序进程” 是一种计算模型,实际上,唯一的同步原语是在通道上发送或接收的。一旦使用 <ruby>互斥量<rt>mutex</rt></ruby><ruby>信号量<rt>semaphore</rt></ruby><ruby>条件变量<rt>condition variable</rt></ruby>、bam你就不再处于纯 CSP 领域。 Go 程序员经常通过高呼 [缓存的思想][21] “[通过交流共享内存][22]” 来宣扬这种模式和哲学。
Hoare 的 “通信顺序进程” 是一种计算模型,实际上,唯一的同步原语是在通道上发送或接收的。一旦使用 <ruby>互斥量<rt>mutex</rt></ruby><ruby>信号量<rt>semaphore</rt></ruby><ruby>条件变量<rt>condition variable</rt></ruby>、bam你就不再处于纯 CSP 领域。 Go 程序员经常通过高呼 “[通过交流共享内存][22]” 的 [缓存的思想][21] 来宣扬这种模式和哲学。
所以,让我们尝试在 Go 中仅使用 CSP 编写一个小程序!让我们成为高分接收者。我们要做的就是跟踪我们看到的最大的高分值。如此而已。
那么,让我们尝试在 Go 中仅使用 CSP 编写一个小程序!让我们成为高分接收者。我们要做的就是跟踪我们看到的最大的高分值。如此而已。
首先,我们将创建一个 `Game` 结构体。
@ -82,7 +79,7 @@ type Player interface {
}
```
为了处理 `player`,我们假设所有错误都是致命的,并将获得的比分向下传递到通道。
为了处理 `Player`,我们假设所有错误都是致命的,并将获得的比分向下传递到通道。
```
func (g *Game) HandlePlayer(p Player) error {
@ -100,7 +97,6 @@ func (g *Game) HandlePlayer(p Player) error {
你圆满完成了自己的开发工作,并开始拥有客户。你将这个游戏服务器公开,就取得了令人难以置信的成功!你的游戏服务器上也许正在创建许多游戏。
很快,你发现人们有时会离开你的游戏。许多游戏不再有任何玩家在玩,但没有任何东西可以阻止游戏运行的循环。死掉的 `(*Game).run` goroutines 让你不知所措。
**挑战:** 在无需互斥量或 panics 的情况下修复上面的 goroutine 泄漏。实际上,可以滚动到上面的代码,并想出一个仅使用通道来解决此问题的方案。
@ -134,17 +130,17 @@ func (g *Game) HandlePlayer(p Player) error {
}
```
你想选择哪一个?不要被欺骗了,以为通道的解决方案可以使它在更复杂的情况下更具可读性和可理解性。<ruby>拆解<rt>Teardown</rt></ruby>是非常困难的。这种拆解若用<ruby>互斥量<rt>mutex</rt></ruby>来做那只是小菜一碟,但最困难的是只使用 Go 专用通道来解决。另外,如果有人回复说发送通道的通道更容易推理,那么这将使我立即采取行动
你想选择哪一个?不要被欺骗了,以为通道的解决方案可以使它在更复杂的情况下更具可读性和可理解性。<ruby>拆解<rt>Teardown</rt></ruby>是非常困难的。这种拆解若用<ruby>互斥量<rt>mutex</rt></ruby>来做那只是小菜一碟,但最困难的是只使用 Go 专用通道来解决。另外,如果有人回复说发送通道的通道更容易推理,我马上就是感到头疼
重要的是,这个特殊的情况可能真的 **很容易** 解决,而通道有一些运行时的帮助 Go 没有提供!不幸的是,就目前的情况来看,与 Go 的 CSP 版本相比,使用传统的<ruby>同步原语<rt>synchronization primitives</rt></ruby>可以更好地解决很多问题,这是令人惊讶的。稍后,我们将讨论 Go 可以做些什么来简化此案例。
重要的是,这个特殊的情况可能真的 **很容易** 解决,而通道有一些运行时的帮助,而 Go 没有提供!不幸的是,就目前的情况来看,与 Go 的 CSP 版本相比,使用传统的<ruby>同步原语<rt>synchronization primitives</rt></ruby>可以更好地解决很多问题,这是令人惊讶的。稍后,我们将讨论 Go 可以做些什么来简化此案例。
**练习:** 还在怀疑? 试着让上面两种解决方案channel-only vs mutex-only在一旦 `bestScore` 大于或等于 100 时,就停止向 `Players` 索要分数。继续打开你的文本编辑器。这是一个很小的玩具问题。
**练习:** 还在怀疑? 试着让上面两种解决方案(只使用通道与只使用互斥量channel-only vs mutex-only在一旦 `bestScore` 大于或等于 100 时,就停止向 `Players` 索要分数。继续打开你的文本编辑器。这是一个很小的玩具问题。
这里的总结是,如果你想做任何实际的事情,除了通道之外,你还会使用传统的同步原语。
#### 通道比你自己实现要慢一些
Go 如此重视 CSP 理论,我认为其中一点就是,运行时应该可以通过通道做一些杀手级的调度优化。也许通道并不总是最直接的基元,但肯定是高效且快速的,对吧?
Go 如此重视 CSP 理论,我认为其中一点就是,运行时应该可以通过通道做一些杀手级的调度优化。也许通道并不总是最直接的原语,但肯定是高效且快速的,对吧?
![][23]
@ -164,7 +160,7 @@ Go 如此重视 CSP 理论,我认为其中一点就是,运行时应该可以
#### 通道与其他并发原语组合不佳
好的,希望我已经说服了你,有时候,你至少还会与除了通道之外的基元进行交互。标准库似乎显然更喜欢传统的同步基元而不是通道。
好的,希望我已经说服了你,有时候,你至少还会与除了通道之外的原语进行交互。标准库似乎显然更喜欢传统的同步原语而不是通道。
你猜怎么着,正确地将通道与互斥量和条件变量一起使用,其实是有一定的挑战性的。
@ -172,14 +168,13 @@ Go 如此重视 CSP 理论,我认为其中一点就是,运行时应该可以
![][26]
实事求是地说Go 通道也有多种缓冲方式。你可以分配一个固定的空间来考虑可能的缓冲以便发送和接收是不同的事件但缓冲区大小是有上限的。Go 并没有提供一种方法来让你拥有任意大小的缓冲区 —— 你必须提前分配缓冲区大小。 *这很好*,我在邮件列表上看到有人在争论,*因为无论如何内存都是有限的*。
实事求是地说Go 通道也有多种缓冲方式。你可以分配一个固定的空间来考虑可能的缓冲以便发送和接收是不同的事件但缓冲区大小是有上限的。Go 并没有提供一种方法来拥有任意大小的缓冲区--你必须提前分配缓冲区大小。 *这很好*,我已经看到有人在在邮件列表上争论,*因为无论如何内存都是有限的*。
What.
What。
这是个糟糕的答案。有各种各样的理由来使用一个任意缓冲的通道。如果我们事先知道所有的事情,为什么还要使用 `malloc` 呢?
没有任意缓冲的通道意味着在 *任何* 通道上的幼稚发送可能会随时阻塞。你想在一个通道上发送,并在互斥下更新其他一些簿记功能吗?小心!你的通道发送可能被阻塞!
没有任意缓冲的通道意味着在 *任何* 通道上的幼稚发送可能会随时阻塞。你想在一个通道上发送,并在互斥下更新其他一些记账吗?小心!你的通道发送可能被阻塞!
```
// ...
@ -194,7 +189,6 @@ s.mtx.Unlock()
有一种方法可以在 Go 中的通道上进行非阻塞发送,但这不是默认行为。假设我们有一个通道 `ch := make(chan int)`,我们希望在其上无阻塞地发送值 `1`。以下是在不阻塞的情况下你必须要做的最小量的输入:
```
select {
case ch <- 1: // it sent
@ -206,23 +200,21 @@ default: // it didn't
综上所述,因为通道上的很多操作都会阻塞,所以需要对哲学家及其就餐仔细推理,才能在互斥量的保护下,成功地将通道操作与之并列使用,而不会造成死锁。
#### 严格来说,回调更强大,不需要不必要的 goroutines.
#### 严格来说,回调更强大,不需要不必要的 goroutines
![][27]
每当 API 使用通道时,或者每当我指出通道使某些事情变得困难时,总会有人会指出我应该启动一个 goroutine 来读取该通道,并在读取该通道时进行所需的任何转换或修复。
呃,不。如果我的代码位于热路径中怎么办?需要通道的实例很少,如果你的 API 可以设计为使用<ruby>互斥量<rt>mutexes</rt></ruby><ruby>信号量<rt>semaphores</rt></ruby><ruby>回调<rt>callbacks</rt></ruby>,而不使用额外的 goroutine (因为所有事件边缘都是由 API 事件触发的)那么使用通道会迫使我在资源使用中添加另一个内存分配堆栈。是的goroutine 比线程轻得多,但更轻量并不意味着是最轻量。
呃,不。如果我的代码位于热路径中怎么办?需要通道的实例很少,如果你的 API 可以设计为使用<ruby>互斥量<rt>mutexes</rt></ruby><ruby>信号量<rt>semaphores</rt></ruby><ruby>回调<rt>callbacks</rt></ruby>,而不使用额外的 goroutine (因为所有事件边缘都是由 API 事件触发的)那么使用通道会迫使我在资源使用中添加另一个内存分配堆栈。是的goroutine 比线程轻得多,但更轻量并不意味着是最轻量。
正如我以前 [在一篇关于使用通道的文章的评论中争论过的][28]lol the internet如果你使用回调而不是通道你的 API 可以 *总是* 更通用,*总是* 更灵活,而且占用的资源也会大大减少。"总是" 是一个可怕的词,但我在这里是认真的。有证据级的东西在进行。
正如我以前 [在一篇关于使用通道的文章的评论中争论过的][28](呵呵,互联网),如果你使用回调而不是通道,你的 API *总是* 可以更通用,*总是* 更灵活,而且占用的资源也会大大减少。“总是” 是一个可怕的词,但我在这里是认真的。有证据级的东西在进行。
如果有人向你提供了一个基于回调的 API而你需要一个通道你可以提供一个回调在通道上发送开销不大灵活性十足。
另一方面,如果有人提供了一个基于通道的 API 给你,而你需要一个回调,你必须启动一个 goroutine 来读取通道 *并且* 你必须希望当你完成读取时,没有人试图在通道上发送更多的东西,这样你就会导致阻塞的 goroutine 泄漏。
另一方面,如果有人提供了一个基于通道的 API 给你,而你需要一个回调,你必须启动一个 goroutine 来读取通道*并且* 你必须希望当你完成读取时,没有人试图在通道上发送更多的东西,这样你就会导致阻塞的 goroutine 泄漏。
对于一个超级简单的实际例子,请查看 [context interface][29] (顺便说一下,它是一个非常有用的包,你应该用它来代替 [goroutine-local storage][16] )。
对于一个超级简单的实际例子,请查看 [context 接口][29](顺便说一下,它是一个非常有用的包,你应该用它来代替 [goroutine 本地存储][16])。
```
type Context interface {
@ -239,6 +231,7 @@ type Context interface {
```
想象一下,你要做的只是在 `Done()` 通道触发时记录相应的错误。你该怎么办?如果你没有在通道中选择的好地方,则必须启动 goroutine 进行处理:
```
go func() {
<-ctx.Done()
@ -255,17 +248,16 @@ go func() {
Done(cb func())
```
首先,现在登录非常容易。签出:`ctx.Done(func() { log.Errorf ("canceled:%v", ctx.Err()) })`。但是可以说你确实需要某些选择行为。你可以这样调用它:
首先,现在日志记录非常容易。看看:`ctx.Done(func() { log.Errorf ("canceled:%v", ctx.Err()) })`。但是假设你确实需要某些选择行为。你可以这样调用它:
```
ch := make(chan struct{})
ctx.Done(func() { close(ch) })
```
瞧!通过使用回调,不会失去表现力。 `ch` 的工作方式类似于用于返回的通道 `Done()`,在日志记录的情况下,我们不需要旋转整个新堆栈。我必须保留堆栈跟踪信息(如果我们的日志包倾向于使用它们);我必须避免将其他堆栈分配和另一个 goroutine 分配给调度程序。
瞧!通过使用回调,不会失去表现力。 `ch` 的工作方式类似于用于返回的通道 `Done()`,在日志记录的情况下,我们不需要启动整个新堆栈。我必须保留堆栈跟踪信息(如果我们的日志包倾向于使用它们);我必须避免将其他堆栈分配和另一个 goroutine 分配给调度程序。
下次你使用通道时,问问你自己,如果你用互斥量和条件变量代替,是否有一些 goroutine 可以消除? 如果答案是肯定的,那么修改代码将更加有效。而且,如果你试图使用通道只是为了在集合中使用 `range` 关键字,那么我将不得不请你放下键盘,或者只是回去编写 Python 书籍。
下次你使用通道时,问问你自己,如果你用互斥量和条件变量代替,是否可以消除一些 goroutine 如果答案是肯定的,那么修改这些代码将更加有效。而且,如果你试图使用通道只是为了在集合中使用 `range` 关键字,那么我将不得不请你放下键盘,或者只是回去编写 Python 书籍。
![more like Zooey De-channel, amirite][30]
@ -273,11 +265,11 @@ ctx.Done(func() { close(ch) })
在通道已关闭的情况下,执行关闭或发送消息将会引发 panics为什么呢 如果想要关闭通道,你需要在外部同步它的关闭状态(使用互斥量等,这些互斥量的组合不是很好!),这样其他写入者才不会写入或关闭已关闭的通道,或者只是向前冲,关闭或写入已关闭的通道,并期望你必须恢复所有引发的 panics。
这是多么怪异的行为。 Go 中几乎所有其他操作都有避免 panic 的方法(例如,类型断言具有 `ok =` 模式),但是对于通道,你只能自己动手处理它。
这是多么怪异的行为。 Go 中几乎所有其他操作都有避免 panic 的方法(例如,类型断言具有 `, ok =` 模式),但是对于通道,你只能自己动手处理它。
,所以当发送失败时,通道会出现 panic。我想这是有一定道理的。但是与几乎所有其他带有 nil 值的东西不同,发送到 nil 通道不会引发 panic。相反它将永远阻塞这很违反直觉。这可能是有用的行为就像在你的除草器上附加一个开罐器可能有用在 Skymall 可以找到)一样,但这肯定是意想不到的。与 nil 映射执行隐式指针解除引用nil 接口隐式指针解除引用未经检查的类型断言以及其他所有类型交互不同nil 通道表现出实际的通道行为,就好像为该操作实例化了一个全新的通道一样。
,所以当发送失败时,通道会出现 panic。我想这是有一定道理的。但是与几乎所有其他带有 nil 值的东西不同,发送到 nil 通道不会引发 panic。相反它将永远阻塞这很违反直觉。这可能是有用的行为就像在你的除草器上附加一个开罐器可能有用在 Skymall 可以找到)一样,但这肯定是意想不到的。与 nil 映射执行隐式指针解除引用nil 接口隐式指针解除引用未经检查的类型断言以及其他所有类型交互不同nil 通道表现出实际的通道行为,就好像为该操作实例化了一个全新的通道一样。
接收稍微好一点。在已关闭的通道上执行接收会发生什么?好吧,那会是有效操作——你将得到一个零值。好吧,我想这是有道理的。奖金!接收允许你在收到值时进行 `ok =`样式的检查,以确定通道是否打开。谢天谢地,我们在这里得到 `ok =`。
接收的情况稍微好一点。在已关闭的通道上执行接收会发生什么?好吧,那会是有效操作——你将得到一个零值。好吧,我想这是有道理的。奖励!接收允许你在收到值时进行 `, ok =` 样式的检查,以确定通道是否打开。谢天谢地,我们在这里得到了 `, ok =`。
但是,如果你从 nil 渠道接收会发生什么呢? *也是永远阻塞!* 耶!不要试图利用这样一个事实:如果你关闭了通道,那么你的通道是 nil