A cancel-by-default variant of nursery?

Regarding child tasks, I see nursery is designed to be wait-by-default, have you considered a variant of it that cancel-by-default ? I mean as soon as the chunk block of a open_nursery() with stmt finishes (assuming await on the principal result got its answer), we leave this block immediately with all pending tasks cancelled.

What this variant is better called, still nursery or you have a better conceptual name?

With Trio’s nursery API, this is simple enough that we haven’t made a separate API for it:

async with trio.open_nursery() as nursery:
    ... do whatever ...
    nursery.cancel_scope.cancel()   # <-- now it's a cancel-by-default nursery

(Going the other way is very slightly more complicated: if nurseries were cancel-by-default then we’d need to add some kind of await nursery.wait_for_all() primitive.)

We’ve also been discussing a more sophisticated version here:

…though it’s now looking like we might end up adding this as a feature for individual tasks in regular nurseries, rather than making a whole new type of nursery.

To me the key defining conceptual features of “nurseries” are that they’re (1) an explicit object, (2) that reifies some lexical concurrency scope, (3) doesn’t let children escape, (4) has some kind of intelligent handling of error propagation/cancellation/etc. I think the exact details of how you do that are going to vary a lot depending on the language, context, etc.

1 Like

Quite sound for nursery being a primitive building block for lexical scoping of concurrency structure.

I just realize I might be pursuing dynamic scoping regarding concurrency (besides contextual callbacks or effects). Gut feeling tells me that cancel-by-default might suite dynamic scoping of concurrency better.

GHC implements light weighted thread in Haskell, much akin to goroutine in Go in the M:N scheduling regard. I baked a parasitic interpreted language Edh as an object layer on GHC’s runtime, mapping Edh thread 1:1 to GHC thread.

A GHC process, or more precisely, GHC RTS (runtime system) silently stops all other threads when the main thread finishes, I currently follows this model, while multiple Edh programs can be started from a single GHC process, the main Edh thread of a program (its descendant threads as well) can use go statement to spawn more threads, and a forkee thread inherit call stack from its forker, so the forker’s exception handler can run in forkee threads upon exception, then any exception if uncaught by handlers, will be throwTo (using GHC’s asynchronous exception mechanism) the main thread, effectively causing program termination, thus all threads belonging to that program terminated.

Inspired by nursery and Trio, I think I need a primitive to delimit the scope of an Edh program, currently I only have Haskell api runEdhProgram , no construct for Edh code to do it. I hesitate to give it a good name and syntax, do you have suggestions?

[Edh] https://github.com/e-wrks/edh

I’ve used cancel-by-default in libdill (structured concurrency for C). But then, of course, you need a special construct to wait for all threads to finish.

That being said, I think the distinction is not purely syntactic. It has to do with how you think of the call stack (or, rather, call tree).

The case of wait-by-default:

Pros: All the threads are equal. How error handling should work is obvious.
Cons: The fact that one thread has to be main (launches other threads) creeps back in via the special “suspended” state that’s occurs only in the main thread. “Call tree” is implied rather than explicit.

The case of cancel-by-default:

Pros: The main thread is explicitly different from the child threads. “Call tree” is clearly visible.
Cons: Main thread has different semantics than child threads. Not obvious how the errors should propagate from the child threads to the main thread.

1 Like

@Martin_Sustrik Great to know the case of libdill ! Looking at the doc, I see bundle roughly corresponding to Trio nursery in libdill’s vocabulary, capturing the concept well.

Maybe for Edh I can use bundle as the keyword to define a type of procedure that serving as thread bundle leader per called, it should speak loudly of its semantics and purpose to its user. There are already plenty purposeful keywords to define various types of procedures, like generator, producer, interpreter besides vanilla method, so shouldn’t be too weird to have another procedure defining keyword there for thread bundle leader. Would you suggest another name to be used as such a keyword ?

And I already put child thread exceptions in a way possibly down to trigger handlers on forker thread’s call frames, so the programmer can choose to handle exceptions anywhere they see fit. Then in case no handler caught an exception, it will be throwTo the leader thread (thanks to GHC’s asynchronous exception infrastructure) as a hard failure, causing the thread bundle killed at all. There is also a defer mechanism for any thread to schedule cleanup on thread termination due to return/throw/cancelled.

So seems all Cons can be overcome in my case :slight_smile: