8 ms·
Go concurrency is just threads. It's a particularly idiosyncratic userland implementation of them. There are two claims here I'd like to unpack further: 1. "I
by pcwalton 8y ago
Go concurrency is just threads. It's a particularly idiosyncratic userland implementation of them.
There are two claims here I'd like to unpack further:
1. "I've measured goroutine switching time to be ~170 ns on my machine, 10x faster than thread switching time." This is because of the lack of switchto support in the Linux kernel, not because of any fundamental difference between threads and goroutines. A Google engineer had a patch [1] that unfortunately never landed to add this support in 2013. Windows already has this functionality, via UMS. I would like to see Linux push further on this, because kernel support seems like the right way to improve context switching performance.
2. "Goroutines also have small stacks that can grow at run-time (something thread stacks cannot do)." This is a frequent myth. Thread stacks can do this too, with appropriate runtime support: after all, if they couldn't, then Go couldn't implement stack growth, since Go's runtime is built in userland on top of kernel threads. Stack growth is a feature of the garbage collection infrastructure, not of the concurrency support. You could have stack growth in a 1:1 thread system as well, as long as that system kept the information needed to relocate pointers into the stack.
Goroutines are threads. So the idea the "Go has eliminated the distinction between synchronous and asynchronous code" is only vacuously true, because in Go, everything is synchronous.
Finally, Go doesn't do anything to prevent data races, which are the biggest problem facing concurrent code. It actually makes data races easier than in languages like C++, because it has no concept of const, even as a lint. Race detectors have long existed in C++ as well, at least as far back as Helgrind.
[1]: https://blog.linuxplumbersconf.org/2013/ocw/system/presentations/1653/original/LPC%20-%20User%20Threading.pdf https://blog.linuxplumbersconf.org/2013/ocw/system/presentat...
- roganartu 8y ago> Finally, Go doesn't do anything to prevent data races, which are the biggest problem facing concurrent code. It actually makes data races easier than in languages like C++, because it has no concept of const, even as a lint. Race detectors have long existed in C++ as well, at least as far back as Helgrind. While it can't guarantee correctness outside of runtime (and even then, obviously only if whatever you are running actually triggers the race), a race detector has been part of the Go core since 2012[1]. [1]: https://blog.golang.org/race-detector https://blog.golang.org/race-detector
- anonacct37 8y agoAlso there's a "best effort" concurrent map access detector that runs even when you don't compile with race detector support enabled.
- tschellenbach 8y agoDoesn't catch everything, but it catches a lot: https://golang.org/doc/articles/race_detector.html https://golang.org/doc/articles/race_detector.html
- taternuts 8y agoIt's great when it works though; I thought I was running into a race condition once but wasn't sure (new to the language), and having something verify it was indeed a race condition was really awesome.
- jadbox 8y agoSo what's preventing Linux from adopting switchto support? I'd love to see the discussion around it.
- pcwalton 8y agoBeats me! The author no longer works at Google from what I can tell—emails to him bounced. I'd love to see 1:1 threading become competitive with M:N for the heaviest workloads. It just plays so much nicer with the outside world than M:N does.
- Matthias247 8y ago> I'd love to see 1:1 threading become competitive with M:N for the heaviest workloads. It just plays so much nicer with the outside world than M:N does. After working lots of years with all of the available async paradigms (eventloops, promises, async-await, observables, etc) I would tend to agree. Even though many of those things (including Rusts futures implementation) are very well-engineered, the integration problems (as e.g. outlined in the what color is your function article) are very real. And the extra amount of understanding that is required to use and implement those technologies might often not justify the gains. One basically needs to understand normal threaded-synchronization as well as async-synchronization (e.g. as with .NET Tasks or Rusts futures) to build things on top of it. Same goes for the extra level of type indirection (Task<T> vs T), or the distinction between "hot" and "cold" tasks/promises. If it would be possible to get 1:1 threading into the same performance region for most normal applications (e.g. everything but a 1 million connection servers) it seems to very favorable.
- Groxx 8y ago>Goroutines are threads This is a thing I have to continually remind newer engs that are using Go. Goroutines have exactly the same memory observability / race conditions / need for locking/etc as any threaded language. Every race bug that exists in e.g. Java also exists in Go. The only real difference for most code is that it pushes you very hard to use channels directly, as they're the only type-safe option. There are some neat tricks lower down, e.g. where you don't have to explicitly reserve a thread for most syscalls (but there are caveats there too, which is why runtime.LockOSThread() exists), which are legitimately nice conveniences. But not a whole lot else.
- Matthias247 8y ago> The only real difference for most code is that it pushes you very hard to use channels directly, as they're the only type-safe option. It is, but it is also a super important difference: Go provides inbuilt libraries to make inter-thread synchronization more bearable for the average user, e.g. channels and waitgroups. These are lot harder to misuse than bare mutexes and condition variables. Since those are not available in other languages and are too complex for most users to implement them on their own (especially when select{} is required), the solutions there often end up more error-prone.
- pcwalton 8y agoI think more languages should have typed channels, but untyped channels are readily available via OS primitives. Unix has had pipe() since 1973.
- Matthias247 8y agoTrue! Haven't thought about it, but it's actually an OS-backed untyped channel, which even supports select(). Guess the differences are: It operates on bytes and not on objects, so a custom protocol is needed on top of it. And it can't provide the same guarantees as an unbounded channel, where there are certain guarantees that sending an item actually reached the other site, and which makes guaranteed resource handoff through channels a bit easier.
- pdeuchler 8y agoNothing you're saying is wrong, but your perspective is totally off. For someone experienced with C++, or say, Rust (ahem), obviously Go is a bit backwards when it comes to concurrency and race conditions. Obviously you can mimic go's goroutine stacks, obviously you can obtain fast thread switching. But Go isn't targeting C++ or Rust, and it's not targeting the domains those languages are best at (although admittedly there are some overlaps between Go and Rust). Go is trying to replace Ruby, Python, and JS. For programmers who only know those languages, or haven't had the opportunity to work in more "heavy" languages, Go makes it dead simple and intuitive to do things that previously would have been completely out of reach. All you're arguing is that if you go farther "down" the stack so to speak you can accomplish everything Go does, which of course is true, since it's turtles all the way down. The fact of the matter is, if someone boots up a brand new linux laptop goroutine switching _will_ be faster than thread switching. That doesn't mean Go is super crazy performant, or better than C++ or Rust, it means someone who's only programming experience is Rails apps can now write performant multi threaded code with orders of magnitude less domain experience. Same with race detectors. Multithreaded Python is a complete minefield for race conditions. Sure, you might not segfault, but you have to think if you use fork() or spawn() depending on the OS, you have to install libraries to detect races, you have to write special tests. With Go all of that comes out of the box and it makes it _easy_. Go does nothing new, and a lot of languages do things a lot better. Erlang is mentioned in other comments, Erlang is a fantastic language for concurrency and a fantastic language in general. It's also incredibly hard to find programmers who code in it, or are willing to learn it. It's also incredibly hard to sell to the business people higher up. It's also very hard to find Ops people who can competently support an Erlang stack. C++ gives you the power to build formally correct real time systems, but it also gives you the power to blow your whole leg off if you don't know _exactly_ what you're doing. I can go even further down the stack with C and assembly but I think you get the point, it's all about tradeoffs. Go allows programmers to reason and think about concurrency without having to worry about linux kernel PRs, without having to worry about how to share memory, without having to worry about stack performance. *edited for clarity
- aczerepinski 8y ago"But Go isn't targeting C++ or Rust.. Go is trying to replace Ruby"... If you listen to Ken Thompson and Rob Pike talk about the first days of Go, it was directly targeting C++. They mention the absurdly slow compilation times of C++ code at Google, and the high complexity of code that folks were writing. I believe Steve Francia's "Standing on the shoulder" talk goes into this, but I don't have time right now to re-watch and make sure I'm citing the right talk.
- weberc2 8y ago> Goroutines are threads. So the idea the "Go has eliminated the distinction between synchronous and asynchronous code" is only vacuously true, because in Go, everything is synchronous. Abstractly, I agree, but practically the distinction is important. In Go you don't have to use a frustrating async interface to get decent performance. You don't have to manage a threadpool or other tricks. You pretty much get the threadlike interface you want to use without the difficulty.
- pcwalton 8y agoLet's be clear: the one-OS-thread-per-connection model does yield decent performance. We used to call async I/O "solving the C10K problem"—i.e. serving 10,000 clients simultaneously. I'm speaking from experience here, having tried to implement M:N and abandoning it in favor of 1:1, which yielded better performance. Can M:N yield better performance than 1:1? Sure, in some circumstances. But I think that, ideally, we should be striving for 1:1 everywhere.
- dboreham 8y agoAll true. But I have been waiting nearly 30 years for a 1:1 implementation that scaled to the #threads I want to have. Still waiting... The point of M:N schemes isn't to achieve better performance but rather to achieve higher scaling.
- amelius 8y agoI'm trying to find out what you mean by "switchto support". Do you have a link?
- amluto 8y agoSee the bottom of the post you’re replying to.
- jules 8y agoThat Go concurrency is threads is the point of the article. It says this explicitly: "You can think of goroutines as threads, it's a fairly good mental model. They are truly cheap threads". If OS threads become as cheap as userland threads then the implementation of userland threads becomes unnecessary, but the conceptual model stays the same. In other languages, asynchronous APIs return futures or have explicit callbacks, whereas synchronous APIs return the type directly, thus creating a type level distinction between asynchronous and synchronous code. Go's model eliminates that distinction even though it calls asynchronous OS APIs under the hood. Go isn't the king of cheap threads, though. Some cilk style task parallelism implementations have figured out clever tricks to make threads and synchronisation even cheaper. They're so cheap in fact, that a recursive fibonacci function that spawns threads for its recursive calls is almost as fast as one that doesn't. This is achieved by spawning those threads lazily: if a core is idle it looks at the call stacks of other cores and retroactively spawns threads for some of the remaining work on those stacks. It steals that work by mutating that call stack in such a way that if that other core returns to the stack frame it will not perform that work itself, but it will use the result computed by the core that stole the work. This not only avoids thread spawning cost unless the parallelism is actually used, but it also avoids synchronisation cost, because synchronisation is only necessary if work was actually stolen. Cilk style task parallelism traditionally focuses on parallelism rather than concurrency, but there's no reason why the same implementation strategies couldn't work for concurrency. OS threads have no hope of beating this.
- lachlan-sneff 8y agoI've never heard of cilk before, but it seems fascinating. Is there any material I could read on how this is implemented?
- nelsonje 8y agoThis is the canonical Cilk paper: http://supertech.csail.mit.edu/papers/cilk5.pdf http://supertech.csail.mit.edu/papers/cilk5.pdf
- 8y ago
- devxpy 8y ago> Finally, Go doesn't do anything to prevent data races, which are the biggest problem facing concurrent code I've been working on a multiprocessing library -- built a wrapper function that makes any function, an atomic operation on the state. https://zproc.readthedocs.io/en/next/user/atomicity.html https://zproc.readthedocs.io/en/next/user/atomicity.html (Since it's protected by the actor model, not locks it's an enforcing mechanism) Do you think this is a step in the right direction?
- IloveHN84 8y agoJust focus on C++