conradludgate
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.

_ph_
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.

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

evanmoran
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.

dang
Related:

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)

atoav
In releated news:

"Considered Harmful" Essays Considered Harmful https://meyerweb.com/eric/comment/chech.html

fijiaarone
Brilliant 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.

SPBS
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.
pphysch
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.

bsaul
After 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.

Thaxll
So 4 years later where is Trio? Looks like OP does not contribute to the lib anymore?
sfvisser
Read 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?

samsquire
I 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/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

yellowapple
I 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.
arunc
We 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/

Supermancho
> https://stackoverflow.com/questions/55273965/how-to-know-if-...

Q&A's like this have contributed to me avoiding Go.

api
Rust will soon have thread::scope() which will be quite awesome for structured concurrency.
javitury
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. 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.

ivan4th
This 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-... https://github.com/go-tomb/tomb/tree/v2

hardware2win
> 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

projektfu
I was struck by a partial similarity to Ada Tasks. Anyone with more knowledge able to contrast the Nursery paradigm with task/rendevous in Ada?
im3w1l
I 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.
maxekman
What confused me most about the article was that the code examples (and the author's library) were not in Go.
dboreham
Bit 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).

bjarneh
Interesting 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
TL;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

[2] https://meyerweb.com/eric/comment/chech.html

[3] https://news.ycombinator.com/item?id=31956840

phs2501
The nursery concept is being added to core Python 3.11 asyncio as the `TaskGroup`.
throwawaymaths
Just 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.

metadat
Please see related TLA+ thread: 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.

mmarq
I 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.
bborud
I 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.

smokey_circles
This 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.

sr.ht