Merge pull request #20573 from gxlct008/gxlct008-patch-go-channels

提交译文
This commit is contained in:
Xingyu.Wang 2021-01-01 08:48:59 +08:00 committed by GitHub
commit 1769bc82e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 461 additions and 443 deletions

View File

@ -1,443 +0,0 @@
[#]: collector: (lujun9972)
[#]: translator: (gxlct008)
[#]: reviewer: ( )
[#]: publisher: ( )
[#]: url: ( )
[#]: subject: (Go channels are bad and you should feel bad)
[#]: via: (https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-should-feel-bad)
[#]: author: (jtolio.com https://www.jtolio.com/)
Go channels are bad and you should feel bad
======
_Update: If youre coming to this blog post from a compendium titled “Go is not good,” I want to make it clear that I am ashamed to be on such a list. Go is absolutely the least worst programming language Ive ever used. At the time I wrote this, I wanted to curb a trend I was seeing, namely, overuse of one of the more warty parts of Go. I still think channels could be much better, but overall, Go is wonderful. Its like if your favorite toolbox had [this][1] in it; the tool can have uses (even if it could have had more uses), and it can still be your favorite toolbox!_
_Update 2: I would be remiss if I didnt point out this excellent survey of real issues: [Understanding Real-World Concurrency Bugs In Go][2]. A significant finding of this survey is that… Go channels cause lots of bugs._
Ive been using Googles [Go programming language][3] on and off since mid-to-late 2010, and Ive had legitimate product code written in Go for [Space Monkey][4] since January 2012 (before Go 1.0!). My initial experience with Go was back when I was researching Hoares [Communicating Sequential Processes][5] model of concurrency and the [π-calculus][6] under [Matt Might][7]s [UCombinator research group][8] as part of my ([now redirected][9]) PhD work to better enable multicore development. Go was announced right then (how serendipitous!) and I immediately started kicking tires.
It quickly became a core part of Space Monkey development. Our production systems at Space Monkey currently account for over 425k lines of pure Go (_not_ counting all of our vendored libraries, which would make it just shy of 1.5 million lines), so not the most Go youll ever see, but for the relatively young language were heavy users. Weve [written about our Go usage][10] before. Weve open-sourced some fairly heavily used libraries; many people seem to be fans of our [OpenSSL bindings][11] (which are faster than [crypto/tls][12], but please keep openssl itself up-to-date!), our [error handling library][13], [logging library][14], and [metric collection library/zipkin client][15]. We use Go, we love Go, we think its the least bad programming language for our needs weve used so far.
Although I dont think I can talk myself out of mentioning my widely avoided [goroutine-local-storage library][16] here either (which even though its a hack that you shouldnt use, its a beautiful hack), hopefully my other experience will suffice as valid credentials that I kind of know what Im talking about before I explain my deliberately inflamatory post title.
![][17]
### Wait, what?
If you ask the proverbial programmer on the street whats so special about Go, shell most likely tell you that Go is most known for channels and goroutines. Gos theoretical underpinnings are heavily based in Hoares CSP model, which is itself incredibly fascinating and interesting and I firmly believe has much more to yield than weve appropriated so far.
CSP (and the π-calculus) both use communication as the core synchronization primitive, so it makes sense Go would have channels. Rob Pike has been fascinated with CSP (with good reason) for a [considerable][18] [while][19] [now][20].
But from a pragmatic perspective (which Go prides itself on), Go got channels wrong. Channels as implemented are pretty much a solid anti-pattern in my book at this point. Why? Dear reader, let me count the ways.
#### You probably wont end up using just channels.
Hoares Communicating Sequential Processes is a computational model where essentially the only synchronization primitive is sending or receiving on a channel. As soon as you use a mutex, semaphore, or condition variable, bam, youre no longer in pure CSP land. Go programmers often tout this model and philosophy through the chanting of the [cached thought][21] “[share memory by communicating][22].”
So lets try and write a small program using just CSP in Go! Lets make a high score receiver. All we will do is keep track of the largest high score value weve seen. Thats it.
First, well make a `Game` struct.
```
type Game struct {
bestScore int
scores chan int
}
```
`bestScore` isnt going to be protected by a mutex! Thats fine, because well simply have one goroutine manage its state and receive new scores over a channel.
```
func (g *Game) run() {
for score := range g.scores {
if g.bestScore < score {
g.bestScore = score
}
}
}
```
Okay, now well make a helpful constructor to start a game.
```
func NewGame() (g *Game) {
g = &Game{
bestScore: 0,
scores: make(chan int),
}
go g.run()
return g
}
```
Next, lets assume someone has given us a `Player` that can return scores. It might also return an error, cause hey maybe the incoming TCP stream can die or something, or the player quits.
```
type Player interface {
NextScore() (score int, err error)
}
```
To handle the player, well assume all errors are fatal and pass received scores down the channel.
```
func (g *Game) HandlePlayer(p Player) error {
for {
score, err := p.NextScore()
if err != nil {
return err
}
g.scores <- score
}
}
```
Yay! Okay, we have a `Game` type that can keep track of the highest score a `Player` receives in a thread-safe way.
You wrap up your development and youre on your way to having customers. You make this game server public and youre incredibly successful! Lots of games are being created with your game server.
Soon, you discover people sometimes leave your game. Lots of games no longer have any players playing, but nothing stopped the game loop. You are getting overwhelmed by dead `(*Game).run` goroutines.
**Challenge:** fix the goroutine leak above without mutexes or panics. For real, scroll up to the above code and come up with a plan for fixing this problem using just channels.
Ill wait.
For what its worth, it totally can be done with channels only, but observe the simplicity of the following solution which doesnt even have this problem:
```
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()
}
}
```
Which one would you rather work on? Dont be deceived into thinking that the channel solution somehow makes this more readable and understandable in more complex cases. Teardown is very hard. This sort of teardown is just a piece of cake with a mutex, but the hardest thing to work out with Go-specific channels only. Also, if anyone replies that channels sending channels is easier to reason about here it will cause me an immediate head-to-desk motion.
Importantly, this particular case might actually be _easily_ solved _with channels_ with some runtime assistance Go doesnt provide! Unfortunately, as it stands, there are simply a surprising amount of problems that are solved better with traditional synchronization primitives than with Gos version of CSP. Well talk about what Go could have done to make this case easier later.
**Exercise:** Still skeptical? Try making both solutions above (channel-only vs. mutex-only) stop asking for scores from `Players` once `bestScore` is 100 or greater. Go ahead and open your text editor. This is a small, toy problem.
The summary here is that you will be using traditional synchronization primitives in addition to channels if you want to do anything real.
#### Channels are slower than implementing it yourself
One of the things I assumed about Go being so heavily based in CSP theory is that there should be some pretty killer scheduler optimizations the runtime can make with channels. Perhaps channels arent always the most straightforward primitive, but surely theyre efficient and fast, right?
![][23]
As [Dustin Hiatt][24] points out on [Tyler Treats post about Go][25],
> Behind the scenes, channels are using locks to serialize access and provide threadsafety. So by using channels to synchronize access to memory, you are, in fact, using locks; locks wrapped in a threadsafe queue. So how do Gos fancy locks compare to just using mutexs from their standard library `sync` package? The following numbers were obtained by using Gos builtin benchmarking functionality to serially call Put on a single set of their respective types.
```
> BenchmarkSimpleSet-8 3000000 391 ns/op
> BenchmarkSimpleChannelSet-8 1000000 1699 ns/o
>
```
Its a similar story with unbuffered channels, or even the same test under contention instead of run serially.
Perhaps the Go scheduler will improve, but in the meantime, good old mutexes and condition variables are very good, efficient, and fast. If you want performance, you use the tried and true methods.
#### Channels dont compose well with other concurrency primitives
Alright, so hopefully I have convinced you that youll at least be interacting with primitives besides channels sometimes. The standard library certainly seems to prefer traditional synchronization primitives over channels.
Well guess what, its actually somewhat challenging to use channels alongside mutexes and condition variables correctly!
One of the interesting things about channels that makes a lot of sense coming from CSP is that channel sends are synchronous. A channel send and channel receive are intended to be synchronization barriers, and the send and receive should happen at the same virtual time. Thats wonderful if youre in well-executed CSP-land.
![][26]
Pragmatically, Go channels also come in a buffered variety. You can allocate a fixed amount of space to account for possible buffering so that sends and receives are disparate events, but the buffer size is capped. Go doesnt provide a way to have arbitrarily sized buffers - you have to allocate the buffer size in advance. _This is fine_, Ive seen people argue on the mailing list, _because memory is bounded anyway._
Wat.
This is a bad answer. Theres all sorts of reasons to use an arbitrarily buffered channel. If we knew everything up front, why even have `malloc`?
Not having arbitrarily buffered channels means that a naive send on _any_ channel could block at any time. You want to send on a channel and update some other bookkeeping under a mutex? Careful! Your channel send might block!
```
// ...
s.mtx.Lock()
// ...
s.ch <- val // might block!
s.mtx.Unlock()
// ...
```
This is a recipe for dining philosopher dinner fights. If you take a lock, you should quickly update state and release it and not do anything blocking under the lock if possible.
There is a way to do a non-blocking send on a channel in Go, but its not the default behavior. Assume we have a channel `ch := make(chan int)` and we want to send the value `1` on it without blocking. Here is the minimum amount of typing you have to do to send without blocking:
```
select {
case ch <- 1: // it sent
default: // it didn't
}
```
This isnt what naturally leaps to mind for beginning Go programmers.
The summary is that because many operations on channels block, it takes careful reasoning about philosophers and their dining to successfully use channel operations alongside and under mutex protection, without causing deadlocks.
#### Callbacks are strictly more powerful and dont require unnecessary goroutines.
![][27]
Whenever an API uses a channel, or whenever I point out that a channel makes something hard, someone invariably points out that I should just spin up a goroutine to read off the channel and make whatever translation or fix I need as it reads of the channel.
Um, no. What if my code is in a hotpath? Theres very few instances that require a channel, and if your API could have been designed with mutexes, semaphores, and callbacks and no additional goroutines (because all event edges are triggered by API events), then using a channel forces me to add another stack of memory allocation to my resource usage. Goroutines are much lighter weight than threads, yes, but lighter weight doesnt mean the lightest weight possible.
As Ive formerly [argued in the comments on an article about using channels][28] (lol the internet), your API can _always_ be more general, _always_ more flexible, and take drastically less resources if you use callbacks instead of channels. “Always” is a scary word, but I mean it here. Theres proof-level stuff going on.
If someone provides a callback-based API to you and you need a channel, you can provide a callback that sends on a channel with little overhead and full flexibility.
If, on the other hand, someone provides a channel-based API to you and you need a callback, you have to spin up a goroutine to read off the channel _and_ you have to hope that no one tries to send more on the channel when youre done reading so you cause blocked goroutine leaks.
For a super simple real-world example, check out the [context interface][29] (which incidentally is an incredibly useful package and what you should be using instead of [goroutine-local storage][16]):
```
type Context interface {
...
// Done returns a channel that closes when this work unit should be canceled.
Done() <-chan struct{}
// Err returns a non-nil error when the Done channel is closed
Err() error
...
}
```
Imagine all you want to do is log the corresponding error when the `Done()` channel fires. What do you have to do? If you dont have a good place youre already selecting on a channel, you have to spin up a goroutine to deal with it:
```
go func() {
<-ctx.Done()
logger.Errorf("canceled: %v", ctx.Err())
}()
```
What if `ctx` gets garbage collected without closing the channel `Done()` returned? Whoops! Just leaked a goroutine!
Now imagine we changed `Done`s signature:
```
// Done calls cb when this work unit should be canceled.
Done(cb func())
```
First off, logging is so easy now. Check it out: `ctx.Done(func() { log.Errorf("canceled: %v", ctx.Err()) })`. But lets say you really do need some select behavior. You can just call it like this:
```
ch := make(chan struct{})
ctx.Done(func() { close(ch) })
```
Voila! No expressiveness lost by using a callback instead. `ch` works like the channel `Done()` used to return, and in the logging case we didnt need to spin up a whole new stack. I got to keep my stack traces (if our log package is inclined to use them); I got to avoid another stack allocation and another goroutine to give to the scheduler.
Next time you use a channel, ask yourself if theres some goroutines you could eliminate if you used mutexes and condition variables instead. If the answer is yes, your code will be more efficient if you change it. And if youre trying to use channels just to be able to use the `range` keyword over a collection, Im going to have to ask you to put your keyboard away or just go back to writing Python books.
![more like Zooey De-channel, amirite][30]
#### The channel API is inconsistent and just cray-cray
Closing or sending on a closed channel panics! Why? If you want to close a channel, you need to either synchronize its closed state externally (with mutexes and so forth that dont compose well!) so that other writers dont write to or close a closed channel, or just charge forward and close or write to closed channels and expect youll have to recover any raised panics.
This is such bizarre behavior. Almost every other operation in Go has a way to avoid a panic (type assertions have the `, ok =` pattern, for example), but with channels you just get to deal with it.
Okay, so when a send will fail, channels panic. I guess that makes some kind of sense. But unlike almost everything else with nil values, sending to a nil channel wont panic. Instead, it will block forever! Thats pretty counter-intuitive. That might be useful behavior, just like having a can-opener attached to your weed-whacker might be useful (and found in Skymall), but its certainly unexpected. Unlike interacting with nil maps (which do implicit pointer dereferences), nil interfaces (implicit pointer dereferences), unchecked type assertions, and all sorts of other things, nil channels exhibit actual channel behavior, as if a brand new channel was just instantiated for this operation.
Receives are slightly nicer. What happens when you receive on a closed channel? Well, that works - you get a zero value. Okay that makes sense I guess. Bonus! Receives allow you to do a `, ok =`-style check if the channel was open when you received your value. Thank heavens we get `, ok =` here.
But what happens if you receive from a nil channel? _Also blocks forever!_ Yay! Dont try and use the fact that your channel is nil to keep track of if you closed it!
### What are channels good for?
Of course channels are good for some things (they are a generic container after all), and there are certain things you can only do with them (`select`).
#### They are another special-cased generic datastructure
Go programmers are so used to arguments about generics that I can feel the PTSD coming on just by bringing up the word. Im not here to talk about it so wipe the sweat off your brow and lets keep moving.
Whatever your opinion of generics is, Gos maps, slices, and channels are data structures that support generic element types, because theyve been special-cased into the language.
In a language that doesnt allow you to write your own generic containers, _anything_ that allows you to better manage collections of things is valuable. Here, channels are a thread-safe datastructure that supports arbitrary value types.
So thats useful! That can save some boilerplate I suppose.
Im having trouble counting this as a win for channels.
#### Select
The main thing you can do with channels is the `select` statement. Here you can wait on a fixed number of inputs for events. Its kind of like epoll, but you have to know upfront how many sockets youre going to be waiting on.
This is truly a useful language feature. Channels would be a complete wash if not for `select`. But holy smokes, let me tell you about the first time you decide you might need to select on multiple things but you dont know how many and you have to use `reflect.Select`.
### How could channels be better?
Its really tough to say what the most tactical thing the Go language team could do for Go 2.0 is (the Go 1.0 compatibility guarantee is good but hand-tying), but that wont stop me from making some suggestions.
#### Select on condition variables!
We could just obviate the need for channels! This is where I propose we get rid of some sacred cows, but let me ask you this, how great would it be if you could select on any custom synchronization primitive? (A: So great.) If we had that, we wouldnt need channels at all.
#### GC could help us?
In the very first example, we could easily solve the high score server cleanup with channels if we were able to use directionally-typed channel garbage collection to help us clean up.
![][31]
As you know, Go has directionally-typed channels. You can have a channel type that only supports reading (`<-chan`) and a channel type that only supports writing (`chan<-`). Great!
Go also has garbage collection. Its clear that certain kinds of book keeping are just too onerous and we shouldnt make the programmer deal with them. We clean up unused memory! Garbage collection is useful and neat.
So why not help clean up unused or deadlocked channel reads? Instead of having `make(chan Whatever)` return one bidirectional channel, have it return two single-direction channels (`chanReader, chanWriter := make(chan Type)`).
Lets reconsider the original example:
```
type Game struct {
bestScore int
scores chan<- int
}
func run(bestScore *int, scores <-chan int) {
// we don't keep a reference to a *Game directly because then we'd be holding
// onto the send side of the channel.
for score := range scores {
if *bestScore < score {
*bestScore = score
}
}
}
func NewGame() (g *Game) {
// this make(chan) return style is a proposal!
scoreReader, scoreWriter := make(chan int)
g = &Game{
bestScore: 0,
scores: scoreWriter,
}
go run(&g.bestScore, scoreReader)
return g
}
func (g *Game) HandlePlayer(p Player) error {
for {
score, err := p.NextScore()
if err != nil {
return err
}
g.scores <- score
}
}
```
If garbage collection closed a channel when we could prove no more values are ever coming down it, this solution is completely fixed. Yes yes, the comment in `run` is indicative of the existence of a rather large gun aimed at your foot, but at least the problem is easily solveable now, whereas it really wasnt before. Furthermore, a smart compiler could probably make appropriate proofs to reduce the damage from said foot-gun.
#### Other smaller issues
* **Dup channels?** \- If we could use an equivalent of the `dup` syscall on channels, then we could also solve the multiple producer problem quite easily. Each producer could close their own `dup`-ed channel without ruining the other producers.
* **Fix the channel API!** \- Close isnt idempotent? Send on closed channel panics with no way to avoid it? Ugh!
* **Arbitrarily buffered channels** \- If we could make buffered channels with no fixed buffer size limit, then we could make channels that dont block.
### What do we tell people about Go then?
If you havent yet, please go take a look at my current favorite programming post: [What Color is Your Function][32]. Without being about Go specifically, this blog post much more eloquently than I could lays out exactly why goroutines are Gos best feature (and incidentally one of the ways Go is better than Rust for some applications).
If youre still writing code in a programming language that forces keywords like `yield` on you to get high performance, concurrency, or an event-driven model, you are living in the past, whether or not you or anyone else knows it. Go is so far one of the best entrants Ive seen of languages that implement an M:N threading model thats not 1:1, and dang thats powerful.
So, tell folks about goroutines.
If I had to pick one other leading feature of Go, its interfaces. Statically-typed [duck typing][33] makes extending and working with your own or someone elses project so fun and amazing its probably worth me writing an entirely different set of words about it some other time.
### So…
I keep seeing people charge in to Go, eager to use channels to their full potential. Heres my advice to you.
**JUST STAHP IT**
When youre writing APIs and interfaces, as bad as the advice “never” can be, Im pretty sure theres never a time where channels are better, and every Go API Ive used that used channels Ive ended up having to fight. Ive never thought “oh good, theres a channel here;” its always instead been some variant of _**WHAT FRESH HELL IS THIS?**_
So, _please, please use channels where appropriate and only where appropriate._
In all of my Go code I work with, I can count on one hand the number of times channels were really the best choice. Sometimes they are. Thats great! Use them then. But otherwise just stop.
![][34]
_Special thanks for the valuable feedback provided by my proof readers Jeff Wendling, [Andrew Harding][35], [George Shank][36], and [Tyler Treat][37]._
If you want to work on Go with us at Space Monkey, please [hit me up][38]!
--------------------------------------------------------------------------------
via: https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-should-feel-bad
作者:[jtolio.com][a]
选题:[lujun9972][b]
译者:[译者ID](https://github.com/译者ID)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]: https://www.jtolio.com/
[b]: https://github.com/lujun9972
[1]: https://blog.codinghorror.com/content/images/uploads/2012/06/6a0120a85dcdae970b017742d249d5970d-800wi.jpg
[2]: https://songlh.github.io/paper/go-study.pdf
[3]: https://golang.org/
[4]: http://www.spacemonkey.com/
[5]: https://en.wikipedia.org/wiki/Communicating_sequential_processes
[6]: https://en.wikipedia.org/wiki/%CE%A0-calculus
[7]: http://matt.might.net
[8]: http://www.ucombinator.org/
[9]: https://www.jtolio.com/writing/2015/11/research-log-cell-states-and-microarrays/
[10]: https://www.jtolio.com/writing/2014/04/go-space-monkey/
[11]: https://godoc.org/github.com/spacemonkeygo/openssl
[12]: https://golang.org/pkg/crypto/tls/
[13]: https://godoc.org/github.com/spacemonkeygo/errors
[14]: https://godoc.org/github.com/spacemonkeygo/spacelog
[15]: https://godoc.org/gopkg.in/spacemonkeygo/monitor.v1
[16]: https://github.com/jtolds/gls
[17]: https://www.jtolio.com/images/wat/darth-helmet.jpg
[18]: https://en.wikipedia.org/wiki/Newsqueak
[19]: https://en.wikipedia.org/wiki/Alef_%28programming_language%29
[20]: https://en.wikipedia.org/wiki/Limbo_%28programming_language%29
[21]: https://lesswrong.com/lw/k5/cached_thoughts/
[22]: https://blog.golang.org/share-memory-by-communicating
[23]: https://www.jtolio.com/images/wat/jon-stewart.jpg
[24]: https://twitter.com/HiattDustin
[25]: http://bravenewgeek.com/go-is-unapologetically-flawed-heres-why-we-use-it/
[26]: https://www.jtolio.com/images/wat/obama.jpg
[27]: https://www.jtolio.com/images/wat/yael-grobglas.jpg
[28]: http://www.informit.com/articles/article.aspx?p=2359758#comment-2061767464
[29]: https://godoc.org/golang.org/x/net/context
[30]: https://www.jtolio.com/images/wat/zooey-deschanel.jpg
[31]: https://www.jtolio.com/images/wat/joel-mchale.jpg
[32]: http://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/
[33]: https://en.wikipedia.org/wiki/Duck_typing
[34]: https://www.jtolio.com/images/wat/michael-cera.jpg
[35]: https://github.com/azdagron
[36]: https://twitter.com/taterbase
[37]: http://bravenewgeek.com
[38]: https://www.jtolio.com/contact/

View File

@ -0,0 +1,461 @@
[#]: collector: (lujun9972)
[#]: translator: (gxlct008)
[#]: reviewer: ( )
[#]: publisher: ( )
[#]: url: ( )
[#]: subject: (Go channels are bad and you should feel bad)
[#]: 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 is not good》 的概要文章来看这篇博文的话,那么我想表明的是,我很惭愧自己被列在这样的名单上。 Go 绝对是我使用过的最不糟糕的的编程语言。在我写作本文时,我想遏制我所看到的一种趋势,那就是过度使用 Go 的一些较复杂的部分。我仍然认为 <ruby>
通道<rt>Channels</rt>
</ruby>可以更好但是总体而言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、我们认为它是目前为止我们使用过的最不糟糕的、符合我们需求的编程语言。
尽管我也不认为我能说服自己不要提及我的广泛避免 [goroutine-local-storage 库][16] (尽管它是一个你不应该使用的 hack但它是一个漂亮的 hack),希望我的其他经历足以证明我在解释我故意煽动性的帖子标题之前知道我在说什么。
![][17]
### 等等,什么?
如果你在大街上问一个有名的程序员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])。
但是从务实的角度来看(也是 Go 引以为豪的Go 把通道搞错了。在这一点上,实现的通道在我的书中几乎是一个坚实的反模式。为什么?亲爱的读者,让我细数其中的方法。
#### 你可能最终不会只使用通道。
Hoare 的 “通信顺序进程” 是一种计算模型,实际上,唯一的同步原语是在通道上发送或接收的。一旦使用 <ruby>互斥量<rt>mutex</rt></ruby><ruby>信号量<rt>semaphore</rt></ruby><ruby>条件变量<rt>condition variable</rt></ruby>、bam你就不再处于纯 CSP 领域。 Go 程序员经常通过高呼 [缓存的思想][21] “[通过交流共享内存][22]” 来宣扬这种模式和哲学。
所以,让我们尝试在 Go 中仅使用 CSP 编写一个小程序!让我们成为高分接收者。我们要做的就是跟踪我们看到的最大的高分值。如此而已。
首先,我们将创建一个 `Game` 结构体。
```
type Game struct {
bestScore int
scores chan int
}
```
`bestScore` 不会受到<ruby>互斥量<rt>mutex</rt></ruby>的保护!这很好,因为我们只需要一个 goroutine 来管理其状态并通过通道来接收新的分值即可。
```
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`。它也可能会返回错误,因为可能传入的 TCP 流可能会死掉或发生某些故障,或者玩家退出。
```
type Player interface {
NextScore() (score int, err error)
}
```
为了处理 `player`,我们假设所有错误都是致命的,并将获得的比分向下传递到通道。
```
func (g *Game) HandlePlayer(p Player) error {
for {
score, err := p.NextScore()
if err != nil {
return err
}
g.scores <- score
}
}
```
好极了!现在我们有了一个 `Game` 类型,可以以线程安全的方式跟踪 `Player` 获得的最高分数。
你圆满完成了自己的开发工作,并开始拥有客户。你将这个游戏服务器公开,就取得了令人难以置信的成功!你的游戏服务器上也许正在创建许多游戏。
很快,你发现人们有时会离开你的游戏。许多游戏不再有任何玩家在玩,但没有任何东西可以阻止游戏运行的循环。死掉的 `(*Game).run` goroutines 让你不知所措。
**挑战:** 在无需互斥量或 panics 的情况下修复上面的 goroutine 泄漏。实际上,可以滚动到上面的代码,并想出一个仅使用通道来解决此问题的方案。
我等着。
就其价值而言,它完全可以只通过通道来完成,但是请观察以下解决方案的简单性,它甚至没有这个问题:
```
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()
}
}
```
你想选择哪一个?不要被欺骗了,以为通道的解决方案可以使它在更复杂的情况下更具可读性和可理解性。<ruby>拆解<rt>Teardown</rt></ruby>是非常困难的。这种拆解若用<ruby>互斥量<rt>mutex</rt></ruby>来做那只是小菜一碟,但最困难的是只使用 Go 专用通道来解决。另外,如果有人回复说发送通道的通道更容易推理,那么这将使我立即采取行动。
重要的是,这个特殊的情况可能真的 **很容易** 解决,而通道有一些运行时的帮助 Go 没有提供!不幸的是,就目前的情况来看,与 Go 的 CSP 版本相比,使用传统的<ruby>同步原语<rt>synchronization primitives</rt></ruby>可以更好地解决很多问题,这是令人惊讶的。稍后,我们将讨论 Go 可以做些什么来简化此案例。
**练习:** 还在怀疑? 试着让上面两种解决方案channel-only vs mutex-only在一旦 `bestScore` 大于或等于 100 时,就停止向 `Players` 索要分数。继续打开你的文本编辑器。这是一个很小的玩具问题。
这里的总结是,如果你想做任何实际的事情,除了通道之外,你还会使用传统的同步原语。
#### 通道比你自己实现要慢一些
Go 如此重视 CSP 理论,我认为其中一点就是,运行时应该可以通过通道做一些杀手级的调度优化。也许通道并不总是最直接的基元,但肯定是高效且快速的,对吧?
![][23]
正如 [Dustin Hiatt][24] 在 [Tyler Treats post about Go][25] 上指出的那样,
> 在幕后,通道使用锁来序列化访问并提供线程安全性。 因此,通过使用通道同步对内存的访问,你实际上就是在使用锁。 被包装在线程安全队列中的锁。 那么,与仅仅使用标准库 `sync` 包中的互斥量相比Go 的花式锁又如何呢? 以下数字是通过使用 Go 的内置基准测试功能,对它们的单个集合连续调用 Put 得出的。
```
> BenchmarkSimpleSet-8 3000000 391 ns/op
> BenchmarkSimpleChannelSet-8 1000000 1699 ns/o
>
```
无缓冲通道的情况与此类似,甚至是在争用而不是串行运行的情况下执行相同的测试。
也许 Go 调度器会有所改进,但与此同时,良好的旧互斥量和条件变量是非常好、高效且快速。如果你想要提高性能,请使用久经考验的方法。
#### 通道与其他并发原语组合不佳
好的,希望我已经说服了你,有时候,你至少还会与除了通道之外的基元进行交互。标准库似乎显然更喜欢传统的同步基元而不是通道。
你猜怎么着,正确地将通道与互斥量和条件变量一起使用,其实是有一定的挑战性的。
关于通道的一个有趣的事情是,通道发送是同步的,这在 CSP 中是有很大意义的。通道发送和通道接收的目的是为了成为同步屏蔽,发送和接收应该发生在同一个虚拟时间。如果你是在执行良好的 CSP 领域,那就太好了。
![][26]
实事求是地说Go 通道也有多种缓冲方式。你可以分配一个固定的空间来考虑可能的缓冲以便发送和接收是不同的事件但缓冲区大小是有上限的。Go 并没有提供一种方法来拥有任意大小的缓冲区--你必须提前分配缓冲区大小。 *这很好*,我已经看到有人在在邮件列表上争论,*因为无论如何内存都是有限的*。
What.
这是个糟糕的答案。有各种各样的理由来使用一个任意缓冲的通道。如果我们事先知道所有的事情,为什么还要使用 `malloc` 呢?
没有任意缓冲的通道意味着在 *任何* 通道上的幼稚发送可能会随时阻塞。你想在一个通道上发送,并在互斥下更新其他一些簿记功能吗?小心!你的通道发送可能被阻塞!
```
// ...
s.mtx.Lock()
// ...
s.ch <- val // might block!
s.mtx.Unlock()
// ...
```
这是哲学家晚餐大战的秘诀。如果你使用了锁,则应该迅速更新状态并释放它,并且尽可能不要在锁下做任何阻塞。
有一种方法可以在 Go 中的通道上进行非阻塞发送,但这不是默认行为。假设我们有一个通道 `ch := make(chan int)`,我们希望在其上无阻塞地发送值 `1`。以下是在不阻塞的情况下你必须要做的最小量的输入:
```
select {
case ch <- 1: // it sent
default: // it didn't
}
```
对于刚入门的 Go程序员来说这并不是自然而然就能想到的事情。
综上所述,因为通道上的很多操作都会阻塞,所以需要对哲学家及其就餐仔细推理,才能在互斥量的保护下,成功地将通道操作与之并列使用,而不会造成死锁。
#### 严格来说,回调更强大,不需要不必要的 goroutines.
![][27]
每当 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 可以 *总是* 更通用,*总是* 更灵活,而且占用的资源也会大大减少。"总是" 是一个可怕的词,但我在这里是认真的。有证据级的东西在进行。
如果有人向你提供了一个基于回调的 API而你需要一个通道你可以提供一个回调在通道上发送开销不大灵活性十足。
另一方面,如果有人提供了一个基于通道的 API 给你,而你需要一个回调,你必须启动一个 goroutine 来读取通道 *并且* 你必须希望当你完成读取时,没有人试图在通道上发送更多的东西,这样你就会导致阻塞的 goroutine 泄漏。
对于一个超级简单的实际例子,请查看 [context interface][29] (顺便说一下,它是一个非常有用的包,你应该用它来代替 [goroutine-local storage][16] )。
```
type Context interface {
...
// Done returns a channel that closes when this work unit should be canceled.
// Done 返回一个通道,该通道在应该取消该工作单元时关闭。
Done() <-chan struct{}
// Err returns a non-nil error when the Done channel is closed
// 当 Done 通道关闭时Err 返回一个非 nil 错误
Err() error
...
}
```
想象一下,你要做的只是在 `Done()` 通道触发时记录相应的错误。你该怎么办?如果你没有在通道中选择的好地方,则必须启动 goroutine 进行处理:
```
go func() {
<-ctx.Done()
logger.Errorf("canceled: %v", ctx.Err())
}()
```
如果 `ctx` 在不关闭返回 `Done()` 通道的情况下被垃圾回收怎么办?哎呀!这正是一个 goroutine 泄露!
现在假设我们更改了 `Done` 的签名:
```
// Done calls cb when this work unit should be canceled.
Done(cb func())
```
首先,现在登录非常容易。签出:`ctx.Done(func() { log.Errorf ("canceled:%v", ctx.Err()) })`。但是可以说你确实需要某些选择行为。你可以这样调用它:
```
ch := make(chan struct{})
ctx.Done(func() { close(ch) })
```
瞧!通过使用回调,不会失去表现力。 `ch` 的工作方式类似于用于返回的通道 `Done()`,在日志记录的情况下,我们不需要旋转整个新堆栈。我必须保留堆栈跟踪信息(如果我们的日志包倾向于使用它们);我必须避免将其他堆栈分配和另一个 goroutine 分配给调度程序。
下次你使用通道时,问问你自己,如果你用互斥量和条件变量代替,是否有一些 goroutine 可以消除? 如果答案是肯定的,那么修改代码将更加有效。而且,如果你试图使用通道只是为了在集合中使用 `range` 关键字,那么我将不得不请你放下键盘,或者只是回去编写 Python 书籍。
![more like Zooey De-channel, amirite][30]
#### 通道 API 不一致,只是 cray-cray
在通道已关闭的情况下,执行关闭或发送消息将会引发 panics为什么呢 如果想要关闭通道,你需要在外部同步它的关闭状态(使用互斥量等,这些互斥量的组合不是很好!),这样其他写入者才不会写入或关闭已关闭的通道,或者只是向前冲,关闭或写入已关闭的通道,并期望你必须恢复所有引发的 panics。
这是多么怪异的行为。 Go 中几乎所有其他操作都有避免 panic 的方法(例如,类型断言具有 `ok =` 模式),但是对于通道,你只能自己动手处理它。
好的,所以当发送失败时,通道会出现 panic。我想这是有一定道理的。但是与几乎所有其他带有 nil 值的东西不同,发送到 nil 通道不会引发 panic。相反它将永远阻塞这很违反直觉。这可能是有用的行为就像在你的除草器上附加一个开罐器可能有用在 Skymall 可以找到)一样,但这肯定是意想不到的。与 nil 映射执行隐式指针解除引用nil 接口隐式指针解除引用未经检查的类型断言以及其他所有类型交互不同nil 通道表现出实际的通道行为,就好像为该操作实例化了一个全新的通道一样。
接收稍微好一点。在已关闭的通道上执行接收会发生什么?好吧,那会是有效操作——你将得到一个零值。好吧,我想这是有道理的。奖金!接收允许你在收到值时进行 `ok =`样式的检查,以确定通道是否打开。谢天谢地,我们在这里得到 `ok =`
但是,如果你从 nil 渠道接收会发生什么呢? *也是永远阻塞!* 耶!不要试图利用这样一个事实:如果你关闭了通道,那么你的通道是 nil
### 通道有什么好处?
当然,通道对于某些事情是有好处的(毕竟它们是一个通用容器),有些事情你只能用它们来做(比如 `select`)。
#### 它们是另一个特殊大小写的通用数据结构
Go 程序员已经习惯于对泛型的争论,以至于我一提起这个词就能感觉到 PTSD (创伤后应激障碍)的到来。我不是来谈论这件事的,所以擦去额头上的汗水,让我们继续前进吧。
无论你对泛型的看法是什么Go的映射、切片和通道都是支持泛型元素类型的数据结构因为它们已经被特殊化为语言。
无论你对泛型的看法如何Go 的映射、切片和通道都是支持泛型元素类型的数据结构,因为它们在语言中已经过特殊处理。
在一种不允许你编写自己的泛型容器的语言中,任何允许你更好地管理事物集合的东西都是有价值的。在这里,通道是支持任意值类型的线程安全数据结构。
所以这很有用!我想这可以省去一些陈词滥调。
我很难把这算作是通道的胜利。
#### Select
使用通道可以做的主要事情是 `select` 语句。在这里,你可以等待固定数量的事件输入。它有点像 epoll但你必须预先知道要等待多少个套接字。
这是真正有用的语言功能。如果不是 `select`,通道将被彻底清洗。但是我的天呐,让我告诉你,有关你第一次决定可能需要在多个事物中选择,但是你不知道有多少项,因此必须使用 `reflect.Select`
### 通道如何才能更好?
很难说 Go 语言团队可以为 Go 2.0 做的最具战术意义的事情是什么Go 1.0 兼容性保证很好,但是很费劲),但这并不能阻止我提出一些建议。
#### 在条件变量上的 Select
We could just obviate the need for channels! This is where I propose we get rid of some sacred cows, but let me ask you this, how great would it be if you could select on any custom synchronization primitive? (A: So great.) If we had that, we wouldnt need channels at all.
我们可以不需要通道!这是我提议我们摆脱一些“圣牛”(神圣不可质疑的事物)的地方,但是让我问你,如果你可以选择任何自定义同步原语,那会有多棒? (答:太棒了。)如果有的话,我们根本就不需要通道了。
#### GC 可以帮助我们吗?
在第一个示例中,如果我们能够使用定向类型的通道垃圾回收来帮助我们进行清理,我们就可以轻松地解决通道的高分服务器清理问题。
![][31]
如您所知Go 具有定向类型的通道。 您可以使用仅支持读取的通道类型(`<-chan`和仅支持写入的通道类型`chan <-` 这太棒了
Go 也有垃圾收集功能。 很明显,某些类型的簿记方式太繁琐了,我们不应该让程序员去处理它们。 我们清理未使用的内存! 垃圾收集非常有用且整洁。
那么,为什么不帮助清理未使用或死锁的通道读取呢? 与其让 `make(chan Whatever)`返回一个双向通道,不如让它返回两个单向通道(`chanReader, chanWriter:= make(chan Type)`)。
让我们重新考虑一下最初的示例:
```
type Game struct {
bestScore int
scores chan<- int
}
func run(bestScore *int, scores <-chan int) {
// 我们不会直接保留对游戏的引用,因为这样我们就会保留着通道的发送端。
for score := range scores {
if *bestScore < score {
*bestScore = score
}
}
}
func NewGame() (g *Game) {
// 这种 make(chan) 返回风格是一个建议
scoreReader, scoreWriter := make(chan int)
g = &Game{
bestScore: 0,
scores: scoreWriter,
}
go run(&g.bestScore, scoreReader)
return g
}
func (g *Game) HandlePlayer(p Player) error {
for {
score, err := p.NextScore()
if err != nil {
return err
}
g.scores <- score
}
}
```
如果垃圾收集关闭了一个通道,而我们可以证明它永远不会有更多的值,那么这个解决方案是完全可行的。是的,是的,`run` 中的评论暗示着有一把相当大的枪瞄准了你的脚,但至少现在这个问题可以很容易地解决了,而以前确实不是这样。此外,一个聪明的编译器可能会做出适当的证明,以减少这种脚枪造成的损害。
#### 其他较小的问题
* **Dup 通道吗?** —— 如果我们可以在通道上使用等效于 `dup` 的系统调用,那么我们也可以很容易地解决多生产者问题。 每个生产者可以关闭自己的 `dup` 版通道,而不会破坏其他生产者。
* **修复通道API** —— 关闭不是幂等的吗? 在已关闭的通道上发送信息引起的 panics 没有办法避免吗? 啊!
* **任意缓冲的通道** —— 如果我们可以创建没有固定的缓冲区大小限制的缓冲通道,那么我们可以创建非阻塞的通道。
### 那我们该怎么向大家介绍 Go 呢?
如果你还没有,请看看我目前最喜欢的编程文章:[你的功能是什么颜色][32]。虽然不是专门针对 Go但这篇博文比我更有说服力地阐述了为什么 goroutines 是 Go 最好的特性(这也是 Go 在某些应用程序中优于 Rust 的方法之一)。
如果你还在使用这样的一种编程语言写代码,它强迫你使用类似 `yield` 关键字来获得高性能、并发性或事件驱动的模型那么你就是活在过去不管你或其他人是否知道这一点。到目前为止Go 是我所见过的实现 MN 线程模型(非 11 )的语言中最好的入门者之一,而且这种模型非常强大。
所以,跟大家说说 goroutines 吧。
如果非要我选择 Go 的另一个主要特性,那就是接口。静态类型 [<ruby>鸭子模型<rt>duck typing</rt></ruby>][33] 使得扩展,使用你自己或他人的项目变得如此有趣而令人惊奇,这也许值得我改天再写一组完全不同的文字。
### 所以…
我一直看到人们争先恐后冲进 Go渴望充分利用通道来发挥其全部潜力。这是我对你的建议。
**JUST STAHP IT**
当你在编写 API 和接口时,尽管“绝不”的建议可能很糟糕,但我非常肯定,从来没有什么时候通道是更好的,我用过的每一个使用通道的 Go API最后都不得不与之抗争。我从来没有想过“哦 太好了这里是一个通道”它总是被一些变体取代_**这是什么新鲜的地狱?**_
所以_请在适当的地方并且只在适当的地方使用通道。_
在我使用的所有 Go 代码中,我可以用一只手数出有多少次通道真的是最好的选择。有时候是这样的。太好了!。那就用它们吧。否则就别说了。
![][34]
_特别感谢我的校对读者 Jeff Wendling、[Andrew Harding][35]、[George Shank][36] 和 [Tyler Treat][37] 提供的宝贵反馈。_
如果你想和我们一起用 Go 在 Space Monkey 项目工作,请[给我打个招呼][38]
--------------------------------------------------------------------------------
via: https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-should-feel-bad
作者:[jtolio.com][a]
选题:[lujun9972][b]
译者:[gxlct008](https://github.com/gxlct008)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]: https://www.jtolio.com/
[b]: https://github.com/lujun9972
[1]: https://blog.codinghorror.com/content/images/uploads/2012/06/6a0120a85dcdae970b017742d249d5970d-800wi.jpg
[2]: https://songlh.github.io/paper/go-study.pdf
[3]: https://golang.org/
[4]: http://www.spacemonkey.com/
[5]: https://en.wikipedia.org/wiki/Communicating_sequential_processes
[6]: https://en.wikipedia.org/wiki/%CE%A0-calculus
[7]: http://matt.might.net
[8]: http://www.ucombinator.org/
[9]: https://www.jtolio.com/writing/2015/11/research-log-cell-states-and-microarrays/
[10]: https://www.jtolio.com/writing/2014/04/go-space-monkey/
[11]: https://godoc.org/github.com/spacemonkeygo/openssl
[12]: https://golang.org/pkg/crypto/tls/
[13]: https://godoc.org/github.com/spacemonkeygo/errors
[14]: https://godoc.org/github.com/spacemonkeygo/spacelog
[15]: https://godoc.org/gopkg.in/spacemonkeygo/monitor.v1
[16]: https://github.com/jtolds/gls
[17]: https://www.jtolio.com/images/wat/darth-helmet.jpg
[18]: https://en.wikipedia.org/wiki/Newsqueak
[19]: https://en.wikipedia.org/wiki/Alef_%28programming_language%29
[20]: https://en.wikipedia.org/wiki/Limbo_%28programming_language%29
[21]: https://lesswrong.com/lw/k5/cached_thoughts/
[22]: https://blog.golang.org/share-memory-by-communicating
[23]: https://www.jtolio.com/images/wat/jon-stewart.jpg
[24]: https://twitter.com/HiattDustin
[25]: http://bravenewgeek.com/go-is-unapologetically-flawed-heres-why-we-use-it/
[26]: https://www.jtolio.com/images/wat/obama.jpg
[27]: https://www.jtolio.com/images/wat/yael-grobglas.jpg
[28]: http://www.informit.com/articles/article.aspx?p=2359758#comment-2061767464
[29]: https://godoc.org/golang.org/x/net/context
[30]: https://www.jtolio.com/images/wat/zooey-deschanel.jpg
[31]: https://www.jtolio.com/images/wat/joel-mchale.jpg
[32]: http://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/
[33]: https://en.wikipedia.org/wiki/Duck_typing
[34]: https://www.jtolio.com/images/wat/michael-cera.jpg
[35]: https://github.com/azdagron
[36]: https://twitter.com/taterbase
[37]: http://bravenewgeek.com
[38]: https://www.jtolio.com/contact/