10 ms·
Goroutines: The concurrency model we wanted all along
- adeon 3y agoHaskell threads have some similarities to Goroutines. They are also real cheap and there's no problem creating lots of threads (M:N scheduling behind the scenes). There's also no function color problem with the threads. There's one special thing that Haskell threads have that I think I've only seen in Erlang: it's safe to kill threads. Things will be cleaned up properly. You can safely expect that Haskell code created by someone else has been written to not go into a bad state if it's suddenly cancelled. For example, you can write a generic timeout function that works by launching the work in a new thread, then wait N seconds, then kill that thread if it's still running. You don't need to make any kind of signalling thing where you send "please stop" message to a Goroutine. The article talks about Goroutines in context of serving lots of requests. I like to think Haskell's threads and the fact that you can kill them safely makes it much easier to develop servers or job orchestration with simpler code.
- aseipp 3y agoTo be fair, there have been plenty of bugs in Haskell libraries wrt exception safety. Cancellation and asynchronous exceptions are actually really hard in practice. But yes, actually having the possibility can make some code a lot easier to write and think about.
- whateveracct 3y agothe flipside is async exceptions mean you can always kill a thread if a goroutine doesn't select on a chan, you're screwed and don't get me started on how bad Go gets in terms of exception safety. I've seen defer go wrong in so many ways. Unrestricted mutability is mostly to blame.
- whateveracct 3y agoYes! People malign async exceptions but the fact that you can zap GHC thread is genius. I am mostly a Haskeller (10 years now) but I used Go at a startup once. It was shocking how often we leaked goroutines. Whereas in Haskell, if you use async it's hard to. Haskell exceptions are tricky (and there are plenty of bloggers who make a niche living acting like they're so hard to deal with you need to hire them) but in 2023 it's trivial. Just use the unliftio wrappers and you're golden.
- zokier 3y agoIt is dubious that the article does not mention structured concurrency even once, despite it being the model du jour
- nitwit005 3y agoIt's not without its downsides. The fact that IO blocks, and also doesn't work with the select statement, means you need to spin up a mess of extra goroutines in some situations. There was some good prior discussion of this here: https://news.ycombinator.com/item?id=13331284 https://news.ycombinator.com/item?id=13331284
- bjoli 3y agoI am still waiting for people to discover concurrentML. Whenever pthreads becomes a hassle I reach for CML and it hasn't failed me yet. It is pretty similar to go's concurrency model, but with dynamic selects that doesn't suck. And it is over 30 years old.
- kaycey2022 3y agoI love Go, but I find the following things contradictory in the design. "Don't communicate by sharing memory, share memory by communicating" from go proverbs[0]. And in the article, "When you create a goroutine, you're essentially allocating a closure and adding it to a queue in the runtime." Because of this it becomes very easy to share memory by mistake and you end up with bugs. On the other hand Elixir's design makes this kind of thing very hard to do by mistake. [0] - https://go-proverbs.github.io/ https://go-proverbs.github.io/
- jpgvm 3y agoIMO JVM + Loom is the better runtime and with Kotlin it has the superior language also. Before Loom there was often a case to pick Go just for goroutines but that time has passed for me at least. Go still has it's place in my toolbox, namely shuffling bytes from one fd to another when such a task is called for in an environment where the JVM would be too cumbersome or I just want redistributable binaries for one reason or another. (though Rust is increasingly consuming these use-cases for me I really don't like async Rust) The call out that Loom doesn't currently do forced preemption/time-slicing is notable though, for now you should make use of platform threads for CPU-heavy parallelism if you have tasks that simply won't yield. The fact you can still just use platform threads whenever you need them largely obviates that from being impactful.
- DeathArrow 3y ago>Before Loom there was often a case to pick Go just for goroutines but that time has passed for me at least Not true in .Net world as async and Tasks were available since long time ago.
- kaba0 3y agoWhich have absolutely nothing in common with Loom.
- kaba0 3y agoIt had the superior language even with Java 1.1.
- mrkeen 3y agoWhat is the benefit of using greenthreads+channels over, say, greenthreads+channels+transactions?
- deleted 3y ago[deleted]
- samsquire 3y agoThanks for this article and to ingve for submitting it. Concurrency and async is my favourite topic. I think event loop programming is very interesting, and Goroutines mean every process is effectively an event loop. So I like server programming. I have an epoll server that multiplexes multiple sockets per thread which is a bit more scalable than a thread per socket. I like nodejs libuv, but I think we can go further. I wrote a very simple toy lightweight 1:M:N (1 scheduler:M kernel threads:N lightweight threads) thread scheduler in C, Rust and Java. https://github.com/samsquire/preemptible-thread https://github.com/samsquire/preemptible-thread It works on the principle that hot loops can be interrupted BY ANOTHER THREAD (the scheduler thread) on a timer by setting a loop to its limit, to give lightweight threads a chance to execute. What I think I want today though is an extremely rich process/concurrency API that resembles a stream API but for processes. For example, we should be able to create pipelines that can be paused, resumed, forked, merged, drop_while, iterate_until and whatever else would be useful.
- pdimitar 3y agoTry Elixir?
- vanillamice 3y agoGenerics do exist though?
- anacrolix 3y agoI thought this would be pro-implicit green threading, of which only Go and Haskell do successfully to my knowledge. Instead it seems to support async/await, which are very different takes.
- commandersaki 3y agoJava is conspicuously absent from the list of languages above. Until now, you've had to either spawn an unreasonable number of threads or deal with Java's particular circle of callback hell. Happily, JEP 444 adds virtual threads, which sound a lot like goroutines. Virtual threads are cheap to create. The JVM schedules them onto platform threads (real threads the kernel knows about). There are a fixed number of platform threads, generally one per core. When a virtual thread performs a blocking operation, it releases its platform thread, and the JVM may schedule another virtual thread onto it. Unlike goroutines, virtual thread scheduling is cooperative: a virtual thread doesn't yield to the scheduler until it performs a blocking operation. This means that a tight loop can hold a thread indefinitely. I don't know whether this is an implementation limitation or if there's a deeper issue. Go used to have this problem until fully preemptive scheduling was implemented in 1.14. TIL, I would've thought from that description that virtual threads and goroutines were identical, but looks like I have an outdated mental model of goroutines from the <= 1.14 era.
- extasia 3y agoI really like this guys writing style. Very easy to digest, nice short sentences that are easily understood.
- singularity2001 3y agoAsync function coloring is a worse disaster than "goto" of the previous generation and go is one of the very few languages that didn't fall into that trap.
- jiveturkey 3y ago> but it marks all your memory as copy-on-write. Each write to a copy-on-write page causes a minor page fault, a small delay that's hard to measure I'm really surprised to hear this called out as problematic. It's actually one of the most efficient aspects of fork() IMHO. COW is almost ideal is it not? The real problem with fork() is that the entire virtual address space has to be cloned. In practice this means that it's hard to avoid overallocating/overcommitting physical memory, which in Linux anyway leads to OOM of random processes. That is truly awful. He also goes on to note that threads are lighter than processes, but the author neglects to mention clone() in Linux. I think that an article written in 2023, hailing goroutines as an alternative to multiprocess server architecture, is severely lacking. > Threads do still have a substantial cost though, and you'll run into scaling problems if you create a new thread for every connection Depends on the threading model, doesn't it? ref. Solaris I appreciate this article but it lacks quite a lot of depth. Reads like a first year student's inner monologue. Which is great, but I'm surprised it has gained HN traction.
- mighmi 3y agoFor a few years, I found myself continuously appreciating Go's design and models more and more. Right now, a bit bored of working in the same paradigm for a decade, I've been trying to find new approaches but find myself rather trapped - as after a novel exploration, it turns out the current approach really does seem to work better. Go's whole concurrency model of Communicating Sequential Processes is cool, although seems to be the aspect I question the most. Erlang's Actor Model does seem a bit superior and Go can introduce footguns at points, but I've yet to really investigate the alternatives as much as I'd like. Also, Beej's guide to network programming https://beej.us/guide/bgnet/html/ https://beej.us/guide/bgnet/html/ mentioned in the beginning really is quite fantastic.
- vore 3y agoThe thing I find weird about Erlang's design is that actor mailboxes have no backpressure mechanism, which seems like a pretty big footgun. Go channels have their own quirks, but requiring them to be bounded was a good design choice.
- littlestymaar 3y ago> Go channels have their own quirks, but requiring them to be bounded was a good design choice. Making it the default is a good design choice, but mandating them isn't: sometimes you don't want backpressure and buffering is what you want, and with no unbounded channel, you easily end up with being an unbounded amount of goroutines instead… (it happened to me once earlier this year)
- superb_dev 3y agoPonylang has backpressure for its actor's
- toast0 3y agoBEAM has some backpressure; it used to have more. If you send to a remote process, and the dist buffer is full, you'll be suspended. (Or you can send with no_suspend, and drop rather than suspend). When you send to a local process, you need to lock that processes's mailbox, which offers a little bit of backpressure if there's contention. Doesn't provide backpressure when the recipient process is just ignoring its mailbox, though. IIRC, earlier releases, maybe around r12-r16 would penalize processes that sent to a local process with a big mailbox, but I think that was removed because it didn't really work as envisioned, so better not to have the complexity. There's ways to build systems that are resilient to large mailboxes, but it's not necessarily simple.
- dekhn 3y agogoroutines are just m:n threads with message passing queues. please correct me if I'm missing some obvious detail that makes them unique in any way. in that case, I've been using this model for a couple decades, as they are similar to green threads. They are in fact a wonderful model for concurrency but I think they leave a number of expert-programmer aspects of threads off the table.
- fidotron 3y agoThe message passing queues following CSP semantics regarding synchronization is the big deal. If you only ever use buffered queues then you're missing half of the story. Occam is probably the most important totally forgotten thread of programming language development.
- dboreham 3y agoNot really forgotten because Phil Winterbottom was familiar with it and went on to work on Go.
- packetlost 3y agoCan you elaborate or provide sources on what you mean by "CSP semantics" here?
- Jtsummers 3y agoThey mean communicating sequential processes. Here's a book on the topic by C.A.R. Hoare: http://www.usingcsp.com/ http://www.usingcsp.com/
- dekhn 3y agoThe message passing queues in CSP aren't any different from buffered queues, right? I've been asking people this for some time and never have heard a convincing "no, and here's why they are different". There are CSP implementations in C++ and Python and their implementations seem to be buffered queues that block if there are no readers.
- synthetigram 3y agoThe StreamObserver API came at a time (2015) when it seems liked RxJava was going to take over. That didn't end up happening, but the API is still around. While it is more cumbersome, some things are /impossible/ to do with the Go style blocking. For example, try cancelling out of a Recv() call. The only way is to tear the entire Stream down. Goroutines never successfully married select {} and sync.Cond, or context Cancel. These are needed to successfully back out of a blocking statement. Unfortunately, that can't be done, and a goroutine that blocks is really stuck there. The only saving grace is that goroutines are relatively cheap (2-4K of memory?), and it's okay if a few O(100K) of them get stuck.
- dfawcus 3y agoAs I recall, cancelling out of a Recv() as in a network read can be achieved by setting an expiry time in the past. i.e. on a net.Conn one can use SetReadDeadline() to unblock/cancel a Read().
- ryanthedev 3y agoWell articulated. Few people realized what callback hell was. Web devs got lucky they added await to JS. I always appreciated Go for its coroutines. Does make for simpler code. You know a language has done well when the biggest consistent argument is that it’s “boring”
- lalaithion 3y agoMy experience with Goroutines is that eventually you run into the limits of working with them and your code explodes in size. Your for item := range channel { // code goes here } becomes for { var item Item var ok bool select { case <-ctx.Done(): break case item, ok = <-channel: if !ok { break } } // code goes here } (If you have a solution to this pattern, please do share! I would _love_ a better way.)
- quadcore 3y agoIt does become a channel mess. The trick I use is to code everything using the same patterns (down to the same characters, name conventions, line breaks, etc) as much as I can so i can use the text editor to work (refactor) that mass. It's definitely the less funny part and why I'll still use clojure and it's async.io module whenever it makes sense (lots of different data). That way, I macro myself out of that problem. Now if you cant use another language, change project. Go is not perl nor python, it's a sophisticated C. Use a sophisticated C for the wrong project / product and you'll want to kill yourself.
- chrsig 3y agoGenerics! func streamingApply[T any](ch channel[T], fn func(T)){ for { select { case <-ctx.Done(): break case item, ok = <-ch: if !ok { break } fn(item) } } } For everyone that's complained about generics being introduced to go...It really does help clean up this type of repetition.