goroutines with channels solve a lot of your objections except that of handling serious errors. By convention you can pass along an error but you can’t pass it back to the caller of the channel.
You might be able to implement a nursery by convention in GoLang by forcing your GoRoutines to write their errors to an Error channel which was monitored by the nursery. By convention most GoRoutines take a “done” channel as an argument. When the nursery decides to stop processing due to an error in on one of the GoRoutines, the error monitoring GoRoutine can close the done Channel.
The fact that there is no test for “is this channel closed” is a hassle because ideally there are multiple conditions under which you would want to close everything down, and ideally you need a broadcast to do it. The hack is that any routine desiring to close the done channel has to send a message on yet another channel “Please close shit down” then the monitoring channel on that one has to close the “done” channel. Then once all these messaging channels are closed, close the “please close down” channel. Doable. but way complex to get it right.
And without any sort of templating mechanism pretty likely to have to be hand coded in every nursery.
Anyway those are my first thoughts without having tried to write all this. I am very familiar with GoLang though and use GoRoutines regularly without your issues.
Is there some discussion on how structured concurrency works with GUI frameworks?
Of course, you can do the trivial thing that you wrap the instantiation of the GUI application in a giant nursery that you pass to it so that all the event handling callbacks can live in it. Has anybody come up with some finer grained approach, and is it even desirable?
You could probably do one per window if you had some windowing framework that abstracts filtering of events by window really well but it does not seem to be common.
I was just made aware of this post when listening to CppCast, where they’re about to talk about thr libunifex library (haven’t listened to the episode yet, decided to come and have a read first). Like others in the thread it was opening to see these thoughts in written form. Everyone knows goto is bad, yet nobody can explain it this clearly. I also didn’t know it was SO bad in earlier languages. And while I always knew somehow that concurrency frameworks had something not quite right, I couldn’t point out what exactly. The control flow black box analogy made it crystal clear.
I haven’t used Trio. While I’ve heard of it, I’d never bothered to understand what set it apart from the rest. I can’t promise I’ll be using it from now on, but the ideas definitely stay.
Another thing: your post also made me think of C’ setjmp and longjmp, which are the unbound gotos of the language. I had never seen them until I started developing extensions for the R language, which internally uses this mechanism to handle errors. Which does exactly what you mention: it completely breaks control flow, making programming these extensions very, very hard (your function might never complete, can’t rely on RAII in C++, etc).
@xialvjun
It’s what FuncSug does : No callback, no thread, just a keyword parallel (and not nursery). But , for now, it’s just for programming in the browser.
But this is the good old system model, which defined the scientific method since the beginning of time!
A system is a black box that is more than the simple addition of its components.
A researcher can focus on the interactions between a system and the world outside the system.
A system can be considered as made up of sub-systems.
I wasted days trying to understand coroutines in Kotlin. All explanations are correct, but they always miss the big picture, and it all appeared insanely complicated.
It seems to me that the so-called escape hatch isn’t really one: if one of the functions spawned inside the nursery calls nursery.start_soon(foo) in its turn, then foo becomes yet another function that the nursery block (the nursery “owner”?) has to wait on before it can continue. Then nothing has really changed and how is that an escape hatch?
Just to check I understand your point, consider this code:
async def outer():
await middle()
async def middle():
async with trio.open_nursery() as n:
await inner(n)
async def inner(n):
n.start_soon(some_other_fn)
After the function “inner(n)” completes, the task it spawns is still running in middle()'s nursery. This is the escape hatch. If nurseries weren’t just normal Python objects that you could pass around, then there would be no way for any function to start a task that outlived its own execution, so it wouldn’t be possible to create a function like inner().
I think your point is something like this: sure, but the task spawned by inner() won’t outlive middle(), because that’s where the nursery is created and therefore also completed. For example, inner() can’t force there to be a task that lives all the way to the end of outer().
That’s true, but no one said that the escape hatch will let you do anything whatsoever. Middle() has effectively given permission to inner() to spawn longer-lived tasks, but it can’t give permission that it doesn’t have itself.
I suppose there are two competing requirements here. (1) You want to be able to look at a function call await foo(...) and be sure when all code associated with it (including child tasks) has completed executing. (2) Sometimes inner functions really do need to spawn tasks that outlive their execution. The escape hatch (being able to pass nurseries as parameters) is a compromise that lets you have both of these.