13 ms·
Notes on structured concurrency, or: Go statement considered harmful (2018)
- dang 4y agoRelated: Go Statement Considered Harmful (2018) - https://news.ycombinator.com/item?id=26509986 https://news.ycombinator.com/item?id=26509986 - March 2021 (82 comments) Notes on structured concurrency, or: Go statement considered harmful - https://news.ycombinator.com/item?id=16921761 https://news.ycombinator.com/item?id=16921761 - April 2018 (230 comments)
- Supermancho 4y ago> https://stackoverflow.com/questions/55273965/how-to-know-if-goroutine-still-exist https://stackoverflow.com/questions/55273965/how-to-know-if-... Q&A's like this have contributed to me avoiding Go.
- EdSchouten 4y agoOut of curiosity, what do you think is wrong with the answers that were provided? Goroutines are purely managed by the Go runtime in userspace. They never ‘just disappear’.
- tgv 4y agoIndeed. It would seem to me that the code calling "isAlive" would serve the same purpose as the panic handler, or a simple flagging mechanism.
- Supermancho 4y ago> Out of curiosity, what do you think is wrong with the answers that were provided? I can't speak to the accuracy. I didn't imply that the answers somehow affect my opinion of Go, but both the questions AND accepted answers. Somehow Go managed to release with a fork that was worse than PHP's first stab, and that's impressively incompetent. Goroutines are the same as js promises without the native state management...and the detachment of declaration and startup? Jesus that's prone to abuse/bugs. Coroutines are a horrific display of the base language on their own. Yes you can create a custom/library wrapper, but that's a leaky abstraction, at best. There are things to be admired about Go's choices, as a language, but there is enough to deride that I avoid bothering to even toy with it.
- orisho 4y agoThe answers are over complicating things though. There are built in libraries that make it trivial to do this. A waitgroup is relatively low level and gives you the function you can call in your defer to signal completion. An errgroup builds on the waitgroup and gives you start & wait primitives, similar to the nursery discussed in the HN post.
- metadat 4y agoPlease see related TLA+ thread: https://news.ycombinator.com/item?id=31956018 https://news.ycombinator.com/item?id=31956018 I've been seriously turned off from go (after dedicating years of my life to it :(). It is fundamentally flawed IMHO, a house of cards built on quicksand. I wonder what we'll think of GO in 5, 10, 25 years. Within 50 years I kinda doubt many humans will give a crap about it at all.
- smokey_circles 4y agoI've never had an issue with data race conditions in go. Can I ask what you did to lose faith? The answer, for me, is always channels. What you put around the channels is the challenging part. Channels are built to pass information between goroutines. If you are doing this via any other signaling, just don't. You can loop over channels for long running sequential reads, you can test them to see if they're ready to read or ready to write. Honorable mention to context as well, extremely useful structure.
- metadat 4y agoI read the Uber article linked in parent. "Data Race Patterns in Go", posted 21 days ago: https://news.ycombinator.com/item?id=31698503 https://news.ycombinator.com/item?id=31698503 Even though I'm extremely careful, I'm sure I've done one or more of the anti-patterns they outline where the outcome is completely incorrect program behavior compared to my intentions as the architect of the program. A good language wouldn't encourage or even compile with such nonsense. --- Isn't it funny how Go offers channels but the stdlib never uses them at all?
- xh-dude 4y agoHmm, the std lib uses channels for a variety of scheduling hook sorts of things - contexts, timers, os signals for example. That’s maybe just a little narrower than how Go users can use them - they aren’t really for transport, but for synchronization (just as useful for no or small payloads as large ones). It doesn’t similarly expose mutexes that package clients have to fiddle with.
- im3w1l 4y agoI like the nursery idea, but the supervisor idea - that someone might silently give you a special nursery that restarts failed tasks sounds like a bad idea. Because the function you pass it to might pass it to someone else that passes it to someone else that passes it to someone else that really doesn't failures retried.
- smokey_circles 4y agoThis opinion is just gross. Always do your required reading kids. - nobody uses semi colons, that's a huge red flag because the tooling will literally remove these symbols from your code, did you even run any go code? clearly not - goroutines are not threads and galloping past that obvious chasm is also telling: you're not really sure what these magical things are but, somehow, you know they're bad. this is a flawed line of reasoning that was never going to convince me - because you don't know how goroutines and the userspace scheduler work, you skip over all the benefits they provide and thusly nurseries give me nothing than a hand holding experience less effective than the existing tools I have. Thank you for wasting my time because I was genuinely holding out for _any_ kind of empirical reasoning. My mistake. - As proof of the author's research negligence: Not a single mention of channels. Not one. Homework was clearly not completed I'm all for debating a language's efficacy and tradeoffs, but not when the opposing side has no idea how anything works. Then it's not a debate, it's just the ignorant proclaiming a need to interfere with others. Go away, please.
- MarkMarine 4y ago- Channels are woefully inefficient. I usually use the stdlib as a good reference for performant code, and I don’t see any use of channels in there. - If you’re not using channels because they are slow, now you’re letting some nebulous “context” bleed into every function to handle closure and cancellation. This is basically provided implicitly by the nursery concept, but stapled on to every function in go because it was an afterthought, then google started using it, so it’s everywhere. - The Unscoped nature of go routines does make the program more complex to reason about.
- hgomersall 4y agoAre you being facetious or did you just not read the post? (I need to ask, because it's not at all obvious!)
- conradludgate 4y agoThis is not a go vs non-go debate. This is a structured concurrency discussion. Threads and goroutines are both forms of concurrency so they can be discussed together.
- atoav 4y agoIn releated news: "Considered Harmful" Essays Considered Harmful https://meyerweb.com/eric/comment/chech.html https://meyerweb.com/eric/comment/chech.html
- ghoward 4y agoI can't take a hypocritical article seriously.
- evanmoran 4y agoI have been using go professionally for a while and I’ve found it to be quite remarkable and I wanted to share a few hints to people so they can find those remarkable parts faster than I did. To get a better sense of go there are five essential concurrency features: 1. Go statements are a nice syntax to run background micro threads (as mentioned in this article) 2. Go Channels pass messages across those threads as fixed memory queues and powerfully as you add items to the queue you block until the item is added (the second part, blocking on add, is very powerful as it prevents the flow of concurrency from infinitely filling queues that are never consumed! There is more to go channels that is possible with fixed memory buffering to prevent the block, but that blocking is key to consider!) 3. Go Select Statements (not switch statements!) let you watch concurrent queues at once in a thread safe way. These are essential for using channels properly and they help manage almost all channel flow (consumer thread progression, error handling, done detection, etc) 4. Go Context objects let you cleanup background threads based on multiple criteria server errors and timeouts being the most common. The best hint you are in a concurrent-friendly function is usually that it passes context as an argument for cleanup purposes. 5. Go wait groups let you wait for all the go statements spawned to finish before proceeding (simple, but essential at times) I know it’s hard to learn the entirety of a langue without using it daily, but I encourage people to try out go to experience these five parts together. Go is truly excellent at expressing some hard concepts well. That doesn’t make it easy — concurrency isn’t easy — but it is easier with go constructs than without.
- t_mann 4y ago> Go statements are a nice syntax to run background micro threads (as mentioned in this article) That's literally the opposite of what the article says. Which btw isn't about Go specifically, but about concurrency in general.
- freiherr 4y agoWell go statements are as nice as you can get with old-school concurrency primitives. Author of the article believes we need to have new completely concurrency primitives.
- hardware2win 4y ago> few languages still have something they call goto, but it's different and far weaker than the original goto. And most languages don't even have that. What happened? This was so long ago that most people aren't familiar with the story anymore, but it turns out to be surprisingly relevant. So we'll start by reminding ourselves what a goto was, exactly, and then see what it can teach us about concurrency APIs. Exceptions? :p
- kaba0 4y agoExceptions only propagate upwards the call stack, which can be equivalently done with return statements and some logic.
- hardware2win 4y agoHmm With return you can jump only 1 level higher at once
- kristov 4y agoPlus exceptions don't return to the call location - control jumps down to the catching block. Exceptions are not goto, but they are not like regular function returns either. Some weird thing in between.
- samsquire 4y agoI don't see the link between golangs go statement and goto except they cause a fork in control paths. Go's go statement is not bad. I wrote a userspace M:N scheduler which multiplexes N lightweight threads onto M kernel threads. It currently preempts lightweight threads and tight loops round robin fashion but I could implement channels between lightweight threads and implement CSP. I created a construct for writing scalable systems concurrently called Crossmerge. It allows blocking synchronous code to be intermingled with while (true) loops and still make progress and join as a nursery does. There is a logical link and orthogonality between blocking and non-blocking and sometimes you need both in the same program. https://github.com/samsquire/ideas4#118-cross-merge https://github.com/samsquire/ideas4#118-cross-merge https://GitHub.com/samsquire/preemptible-thread https://GitHub.com/samsquire/preemptible-thread I added a multiconsumer multiproducer RingBuffer ala LMAX disruptor to the M:N thread multiplexer and it handles IO in a separate thread. If I add epoll and io_uring I could also handle asynchronous IO. My goal is to add an LLVM JIT and then I have an application server. I write a lot of concurrency and parallelism in ideas4 HTTPS://GitHub.com/samsquire/ideas4
- gwd 4y ago> I don't see the link between golangs go statement and goto except they cause a fork in control paths. This is a lot of why he goes into the history of "old-school goto" and its problems in the post. One of the issues with "old-school goto" is that you could never be sure if you called into a function whether it would actually return out of that function, or end up "goto"-ing somewhere else completely different, without going into it and inspecting it. Similarly, one of the issues with calling a function in Golang is that you can't be sure it didn't spin off a goroutine which is still doing some random work or other. I mean, yeah, you shouldn't just randomly fork off a goroutine which retains references to state passed into a function without clearly documenting it. But there's nothing to stop you from doing it.
- Spivak 4y ago> But there's nothing to stop you from doing it. And the crucial point is that because you can do it languages have to have weaker guarantees because they have to assume you could do it.
- javitury 4y agoWhat about async/await? It seems to be a safe pattern under the criteria stated in the article as long as all async calls are awaited. This pattern guarantees the flow will return back only after all async task have finished. The article says modern languages allow domesticated forms of goto (statements like break, continue, return...) that are scoped at function level. What about exceptions and exception handling? An exception thrown in a function and caught in a parent works as a wild goto. The railway pattern comes to my mind.
- masklinn 4y ago> What about async/await? > It seems to be a safe pattern under the criteria stated in the article as long as all async calls are awaited. Which means it's not unless that awaiting is mandatory in all cases. Which it is not because pretty much all such systems have a `spawn`-type construct (when it's not the plain default as it is in Javascript, or C# IIRC) which just forks off an independent task. Your `await` acts as a `join`, but that's still just a join, which is optional, and which most systems have (except go, because goroutines don't have handles). The argument of structured concurrency is the binding of mandatory joining (or awaiting) to the lexical scope, in the way structured flow bound jumps to lexical scopes.
- conradludgate 4y agoAsync await is a different concept to structured concurrency. It just allows that two async tasks can run on the same work thread, but it doesn't directly give you access to do so. That's what goroutines, task spawn methods or structured concurrency give you. At least in rust, calling an async function won't do anything by itself. It must be spawned or awaited. However, I wouldn't be surprised if other languages spawned on every async call and the await was just a join handle.
- sfvisser 4y agoRead the article but honestly don’t fully understand it. How does using this not end up with one big nursery started somewhere in your main passed down everywhere and basically scoped to the entire app lifetime? Getting you in back in the same spot as before. My (rust) code using Tokio starts a bunch of tasks in main that live for the entire lifetime of the app. They’re independent and communicate over channels with each other – and possibly the outside world. Hard to see what problems this causes that Trio can fix. But maybe I’m zooming in on the wrong use case?
- MarkMarine 4y agoKind of a contrived example but let’s say you’re going to kick off a job that downloads a “Post” and associated data to display it, like a “Profile” and “Comments” (with sub comments, profiles, etc) but you decide that hitting an unhandled error in the Profile loading stage should cancel loading all the rest of the sub-jobs you’ve kicked off. Nurseries do this for you, you scoped the whole chunk of concurrent operations and if an unhandled exception bubbles up to the nursery supervisor, you can cancel all the subsequent parts of it. Then you can handle that issue if you choose to, or just ignore loading that post. If you scope that to the whole program, you’d have to handle that failure in the whole program. If you scope these things more finely, you can just cancel and retry or ignore that chunk, without bleeding that logic down into the stack and increasing complexity.
- sfvisser 4y agoI can see how this works, but my personal experience is that concurrent code that is finely scoped is easier to reason about than larger-scoped concurrency already anyways. Hardly a controversial statement, I guess. So if nurseries help me reasoning about finely scoped stuff even better that's great of course, but only solves part of 'the problem'. And maybe not the most interesting part. Just quickly running a task to perform a handful of stuff concurrently for a single purpose (like doing a few network requests and packing the results) is hardly where I encounter big issues. The compositional behavior of Futures really helps here I think. A bunch of `Future<Result<A, Error>>` go a long way.
- maxekman 4y agoWhat confused me most about the article was that the code examples (and the author's library) were not in Go.
- conradludgate 4y agoBecause the article isn't about go. It's about structured concurrency. Goroutines were just a convenient placeholder for the current unstructured concurrency primitives most languages have (and it fit their goto/go narrative)
- conradludgate 4y agoCan the discussions here try to stay away from Go bashing. This post is not about Go. It's about Structured concurrency VS background tasks. There's many interesting discussions one can have about the latter but the former turns into toxicity. With that said. The Rust teams are very interestes in structured concurrency at the moment. Rust 1.63 is going to get scoped threads, which is structured concurrency for threading. Myself and others have also been looking into structured async, albeit it's not as easy to implement. I personally love the concept and I hope it takes off. You rarely need long running background tasks. If you do, you probably want to have a daemon running along side your main application using structured primitives and then dispatch work to them instead. This really simplifies the mental model when you get used to it.
- jpgvm 4y agoThe OpenJDK team is also persuing structured concurrency so we should have multiple interpretations shortly to compare. Exciting stuff for folks that write highly concurrent software.
- zasdffaa 4y agoBut should they be writing highly concurrent software, without thinking bloody hard first I mean? I can see this being the next Big Data or NoSQL 'must have' bandwagon.
- masklinn 4y ago> But should they be writing highly concurrent software, without thinking bloody hard first I mean? Yes, by necessity. We're 15 years after the end of the race for frequency, and the end of moore's law is getting ever closer. Concurrency and parallelism are becoming table stakes for both reactivity and performances. This means making them reliably usable and composable is getting more and more important.
- zasdffaa 4y ago@ohgodplsno (edit, and now @jpgvm's) have good answers grounded in a specific scenario which really makes their point well, but yours worries me. >> But should they be writing highly concurrent software, without thinking bloody hard first I mean? > Yes, by necessity. So we should not think about it first, just do it? And not consider whether there are better solutions such as checking we're using the right algos or cache-aligning our accesses or using a probabilistic filter before hitting the DB or.. Edit: no offense intended, the original question was meant as a rhetorical device to warn against dumbness and bandwaggoning. I am sure you too would not plough in unthinkingly but I'm afraid our industry's reaction is too often to do just that and it's such an expensive mistake, repeated over and over. It's a thoughtless jumping ahead that will lead to slower software, not faster. And buggier.
- _ph_ 4y agoInteresting article, which gives some good idea how to structure concurrent programs with less shooting at your feet :). A bit long winded until it comes to the actual core concept being presented: spawn concurrent routines within a block which doesn't exit until the last routine has exited. This is a good concept an can make a lot of code clearer. It prevents some data races, as you can reason that after the block no concurrent operations are still ongoing, but of course it could make the block lock up by itself, if one of the routines doesn't finish. This is certainly an interesting concept I might try out myself more specifically. It is also important to know, especially as the "go" statement of the language Go is put into the headline, that this very language already supports nursery-like structures, just doens't have the syntax sugar the python extension of the author has. It is called a wait group. See for an example here: https://gobyexample.com/waitgroups https://gobyexample.com/waitgroups So, except for the syntactic sugar around it, the nurseries compare to creating a wait group in a block, spawn goroutines which each call Done() on the wait group on exit and at the end of the block call Wait() to errect the same boundary, nurseries do. Of course you can also pass the waitgroup object to any goroutine created inside goroutines. This is a very common pattern in Go, but indeed, it probably should be presented more clearly and up front in tutorials about goroutines. So for that, I will keep the article around, it shows the concept nicely - perhaps I might do a pure "go" version of it, which then shows the go implementation of nurseries. Might be nice to add to the original article, that not only the presented python library is a solution, but also that there is a native go way of achieving that, as the article uses "go" as the negative example :p
- conradludgate 4y agoI think you missed the point of the article. Its suggesting that goroutine style concurrency should be completely replaced. Much like if/for etc has replaced goto, structured concurrency can replace goroutines. Note: goto can be used like an if or a for loop. You've just made the argument that goroutines can be used like 'nurseries'. You've essentially argued in favour of goto from the articles perspective I want to note again that this article is not actually about go. It is the same in most languages with concurrency primitives. The key takeaway is that having to manually manage waitgroups is room to make mistakes or to introduce spaghetti concurrency, while you might be used to it at this point, it doesn't make it the best system for the future of concurrency
- mmarq 4y agoI have similar concerns on go routines. The good thing is that in practice 99% of microservices in line-of-business applications don’t need concurrency, so go routines don’t cause too many problems.
- bsaul 4y agoAfter a long time experimenting with a lot of patterns, i found the "operationqueue & operation" building blocks from objective-c the most versatile and powerful construct. They let you do all those things the other alternatives i've tried often fall short : - you can cancel them - you can pass a pointer to the operation from places to places - you can set dependencies between operations so that one doesn't start until another is finished - you can set its execution priority (on the queue) Syntax may not be the best, and there may be a few problems with encapsulation (an operation can do any kind of memory manipulation), yet i keep getting back to them whenever i have to do serious and robust work.
- bjarneh 4y agoInteresting article; although goto statements does seem worse than go statements; as they can make simple things hard. I wonder if we just ever come to the conclusion that doing many things at once is difficult no matter what we do - at least if there is shared data involved...
- cb321 4y agoTL;DR - the article seems to invent its own terminology for what many others might know as (roughly) "OMP parallel for" or a fork-join pattern [1] or even in POSIX shell a "a & b & c & wait" pattern. IIRC, the Python multiprocessing module calls it "imap_unordered". The "map" part of "map-reduce". Etc. I'm sure it has many names in both parallel and concurrent contexts which (at this level) share some control flow concepts. When this pattern applies, it is indeed easier to use than less structured alternatives and error handling & resource clean-up & API design are perennial topics, but I'm not sure we need even more diverse terminology. For example, what the article dubs "nursery", most call a "pool" or "group". "Nurseries" are declared a new thing in a boldfaced paragraph with innovation claims only softened later or in footnotes. I think this is why a lot of pushback happens, but also see the funny [2] mentioned elsethread [3]. I did love the domesticated-wolf comparison pictures, though. :) (And I say that earnestly, not snarkily.) [1] https://en.wikipedia.org/wiki/Fork%E2%80%93join_model https://en.wikipedia.org/wiki/Fork%E2%80%93join_model [2] https://meyerweb.com/eric/comment/chech.html https://meyerweb.com/eric/comment/chech.html [3] https://news.ycombinator.com/item?id=31956840 https://news.ycombinator.com/item?id=31956840
- ivan4th 4y agoThis might help with some of the issues with go statement mentioned in the article: https://blog.labix.org/2011/10/09/death-of-goroutines-under-control https://blog.labix.org/2011/10/09/death-of-goroutines-under-... https://github.com/go-tomb/tomb/tree/v2 https://github.com/go-tomb/tomb/tree/v2
- bborud 4y agoI didn't read all of the article. There is something to be said about being able to get your piece across without it becoming story-time. I think the author could have benefitted from getting their point across with fewer words. I think I understand the gist of where the author wants to go, and I do agree that nurseries sound like a good idea. From observing myself and people around me, I think people learn Go (concurrency) in several phases. I won't be so arrogant as to presume I understand how to always structure goroutines and channels correctly, now. But looking back it was "easy", then it got harder, then it got easier again as my understanding of how to structure things evolved. But I won't say that it is ever easy. Concurrency isn't easy. And it is important to keep in mind that stuff that is supposed to help you won't necessarily make everything easier. Concurrency is still fundamentally hard. The hard bit is to actually design this idea in a way that doesn't make Go ugly or which creates bifurcations in how you use the language. If something causes a bifurcation I would actually claim that it isn't worth doing. There is value in leaving languages alone rather than complicate and potentially make them messy. Languages become harder to use when you have to start deciding on what parts of the language you want to use. Adding stuff to a language with established practices is much harder than designing a language to begin with. Also, syntax matters. I didn't actually mind that Java involved a lot of typing because the syntax wasn't that complex or particularly ugly. It wasn't hard to grasp when reading. Just horribly verbose. But comparing how people I met thought about Java in relation to other languages taught me something important: a surprising number of programmers live at the surface, only caring about syntax, and don't actually understand what the syntax really does. If someone can implement the idea I'll have a look. But if it complicates, bifurcates or makes Go exhibit less developer empathy, it probably shouldn't be inflicted on Go. Perhaps it can be applied elsewhere. And I actually have a recommendation for where to start: scrap the posting and attempt a concise description first.
- Thaxll 4y agoSo 4 years later where is Trio? Looks like OP does not contribute to the lib anymore?
- SPBS 4y agoA nursery is just an errgroup (https://pkg.go.dev/golang.org/x/sync/errgroup https://pkg.go.dev/golang.org/x/sync/errgroup). I almost never have to use the `go` keyword directly, only through errgroups. Now I can see that it's because `go` is usually too low level to be used on its own. Not sure I agree with removing `go` entirely though.
- api 4y agoRust will soon have thread::scope() which will be quite awesome for structured concurrency.
- fijiaarone 4y agoBrilliant solution, terrible name. Took me a while to get it — child processes belong in nurseries. Bad abstraction, because the key here is processes. Lots of thing haves child nodes. And what happens in nurseries? Growing? Maybe they were thinking watching — babysitting and it’s a cultural terminology difference. But it would be just as silly to call a thread monitor/manager a babysitter as a nursery. Like I said, it’s a great concept and a valuable abstraction, but I fear it will need a better name to take off.
- Avtomatk 4y agoI hear the term "threadset" in other discussion about structured concurrency, i think "threadset" would be a better name.
- avgcorrection 4y ago> Took me a while to get it — child processes belong in nurseries. Bad abstraction, because the key here is processes. Abstractions have nothing to do with their names. They are not good/bad based on what they are called. You might be conflating metaphors/analogies with that.
- arunc 4y agoWe have stayed away from Go exactly for this reason. The resulting entropy with Go is much higher. This is a good article for sure. But it's just my opinion that Martin Sustrik who originally coined the term structured concurrency explained the concept better than this article at much better depth https://250bpm.com/blog:71/ https://250bpm.com/blog:71/
- phs2501 4y agoThe nursery concept is being added to core Python 3.11 asyncio as the `TaskGroup`.
- pphysch 4y agoThe article does not make clear whether the cases where Go struggles with concurrency are also cases where structured concurrency improves the situation. Pretty sure "sync.WaitGroup is too hard to reason about" is not a real issue people are having. AFAIK most of the challenges occur within that structured block, so to speak. Robust communication between concurrent processes is the hard part, not managing their basic lifecycle.
- projektfu 4y agoI was struck by a partial similarity to Ada Tasks. Anyone with more knowledge able to contrast the Nursery paradigm with task/rendevous in Ada?
- cb321 4y agoI am no expert on Ada, but it was probably inspired by Dijkstra's 1965 Cooperating Sequential Processes with his "parbegin/parend" additions to Algol 60. Or perhaps by the wildly popular Communicating Sequential Processes by Hoare in 1985 (the 3rd most cited thing in Computer Science for over 20 years after publication) which called the control brackets "cobegin/coend" in Hoare's section 7.2.2. [1] [1] https://en.wikipedia.org/wiki/Communicating_sequential_processes https://en.wikipedia.org/wiki/Communicating_sequential_proce...
- dboreham 4y agoBit unclear (the article wastes much space talking about goto), but I _think_ the author has re-invented Occam's PAR construct. Edit: I see someone else noted this below. I had always assumed that golang's concurrency model was somewhat influenced by Occam, via ALEF (Phil Winterbottom).
- throwawaymaths 4y agoJust use elixir or Erlang already. You get all of these things with the simplicity of uncolored functions, plus additional guarantees and customizability about blast radiuses of crashes.
- yellowapple 4y agoI feel like the article understates Erlang's approach to structured concurrency. Yes, you can spawn processes willy-nilly, but there's a strong emphasis in OTP on constructing supervision trees such that child processes don't outlive parents (that being the main problem which Trio, too, seems to address). Even at a lower level, using `spawn_link` instead of `spawn` would readily address that issue.