Can 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.
Structured async sounds like a very interesting idea, this is definitely a space where it feels intuitively as though there should be a better way to express what we mean and get roughly the same machine code but better human understanding for both author and future maintenance programmers - if somebody nails this it could be as big a deal as the loop constructs in modern programming.
Structured programming can guarantee that the control flow graph has constant treewidth[0], which enabled the use of parameterized algorithms for efficient static analysis. Wondering if structured concurrency can impose some additional constraints on the ordering of tasks that makes it easier to analyze, e.g. for linearizability or other properties.
[0]: Mikkel Thorup. 1998. All structured programs have small tree width and good register allocation
The 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.
Interesting 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.
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
I 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
No, I didn't miss the point of the article. I just feel, it is important to note that the proposed style of concurency is available in go and point out to how you would implement it, if you cannot use this Python library.
I would agree, it would be nice to have a similar syntactic construct in go to enforce this pattern, though it is actually possible to basically implement that with higher order functions.
And yes, I think goto has a place in any language, if you are aware of how you should and how you shouldn't use it. In most cases the typical constructs of structured programming are preferrable, but not in all.
Its not the same. Anything in the nursery can cause cancellation of all async work. Cancellation is safe to unwind in all workers by scope exit rules.
Completion is just a type of cancellation.
Spawn 10 workers to lookup Dog in 10 different dictionaries, the first one to get the answer wins. This is hard to do with out language/runtime cancellation support.
This was also my immediate thought, wait groups and a channel for any error would work just like the nursery.
However I still see the advantage for this new approach as a provider for sane defaults.
Right now you need to structure the concurrent routines yourself and are free to mess up. Nurseries should make it the other way around. I'm not sold on that but may give it a chance.
At best, waitgroups are to the structured concurrency as gotos are to loops/ifs(structured programming)--you can implement the latter in terms of the former but the structured part is better (even if/because it is less powerful) as the history shows.
I 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.
Well 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.
> 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.
A nursery is just an 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.
The 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.
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.
I have used structured concurrency with Kotlin and you are right, it is absolutely easier to reason about concurrent code that way.
https://elizarov.medium.com/structured-concurrency-722d765aa...
[0]: Mikkel Thorup. 1998. All structured programs have small tree width and good register allocation
I can see this being the next Big Data or NoSQL 'must have' bandwagon.
It is called a wait group. See for an example here: 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
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
I would agree, it would be nice to have a similar syntactic construct in go to enforce this pattern, though it is actually possible to basically implement that with higher order functions.
And yes, I think goto has a place in any language, if you are aware of how you should and how you shouldn't use it. In most cases the typical constructs of structured programming are preferrable, but not in all.
Completion is just a type of cancellation.
Spawn 10 workers to lookup Dog in 10 different dictionaries, the first one to get the answer wins. This is hard to do with out language/runtime cancellation support.
Note below is a practical lib to get close to this https://blog.labix.org/2011/10/09/death-of-goroutines-under-... .. https://github.com/go-tomb/tomb/tree/v2
It is trivial if the workers can communicate using a shared flag that represents "Dog is found."
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.
That's literally the opposite of what the article says. Which btw isn't about Go specifically, but about concurrency in general.
Go Statement Considered Harmful (2018) - 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 - April 2018 (230 comments)
"Considered Harmful" Essays Considered Harmfulhttps://meyerweb.com/eric/comment/chech.html
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.
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.
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.