* Does anything else like this currently exist (other than the Trio library he mentions), which shows that it's a superior paradigm in practice?
* What are the cons to this approach? Why not do it?
* Does anything else like this currently exist (other than the Trio library he mentions), which shows that it's a superior paradigm in practice?
* What are the cons to this approach? Why not do it?
I only see his nursery as being useful when you really want your async tasks to complete before the function in which they were dispatched returns. That's far from covering every use case of concurrency!
A lot of the value of concurrency is in background operations. These simply can't be tied to the duration of a function call on the dispatch thread. Doing so will literally kill any advantages of concurrency in the first place, might as well just block directly. (This is especially true of apps modelled as an update loop, you definitely don't want to block that loop.)
I can think of very few places where I'd actually want a nursery, and even in those cases I'd rather use the promises or fork&join already available.
His section "There is an escape" answers this criticism: "The nursery object also gives us an escape hatch. What if you really do need to write a function that spawns a background task, where the background task outlives the function itself? Easy: pass the function a nursery object."
He goes on to explain a bit more, but one of his points is what really ties this abstraction back into the goto discussion: "Since nursery objects have to be passed around explicitly, you can immediately identify which functions violate normal flow control by looking at their call sites, so local reasoning is still possible."
> A lot of the value of concurrency is in background operations. These simply can't be tied to the duration of a function call on the dispatch thread.
I think they can: applications with background threads would have an outer-level nursery at whatever "main" is for them, and that nursery would be passed into whatever function needs to spawn a background thread.
Seems like we're just layering indirections (I wouldn't call that an abstraction) for little added value over the existing constructs.
> Much like you know that control flow will come back to the function you're reading after calling another function.
What about continuations? Exceptions? Aborts? setjmp()? I can think of many cases where control doesn't return to the caller that are perfectly valid.
Basically, either the code is so simple theres obviously no bugs, or the code is so complex theres no obvious bugs. I feel a nursery is closer to the later than the former.
TL;DR, this doesn’t add expressive power (in fact, it removes it). The author argues that the power removed is power that is unnecessary, dangerous, and inhibits the ability to infer higher-level invariants about your programs that can improve understandability while potentially enabling even better abstractions. Even more briefly: restrictions make your code better.
Implementing a promise as a monad will yield all the same benefits while also keeping the ability to compose and be immutable; and then you have an abstraction and the result is simple.
I agree restrictions make the code better, this just isn't one of these cases to me.
If your background operation has those characteristics, maybe it is better to spawn a new process for it. Why a thread?
You wouldn't spawn a process for the rendering/input/physics/network/job/loader threads of a game engine for one. You can't spawn processes from a web app either.
Threads are actually pretty darn simple when you have either immutable data or uniqueness constraints. Deterministic parallelism is also very powerful while preventing all sorts of nasty bugs.
This can change if OSes start optimizing for the proposed convention.
> It also adds complexity to the build pipeline.
At least in C/C++ it doesn't. But again, this is also easily solvable if we want to.
> You wouldn't spawn a process for the rendering/input/physics/network/job/loader threads of a game engine for one.
Why not? In my mind, if you need 2-way communication between 2 threads, they should be siblings. If they need one way communication, there should be a parent-child structure. If they need no/minimal communication, they are better off as processes. I don't know which categories each of the threads you named fall into. I realize that this approach will require extensive redesign of existing software, very much like the elimination of goto required.
> You can't spawn processes from a web app either.
No reason for it to stay that way.
> Threads are actually pretty darn simple when you have either immutable data or uniqueness constraints. Deterministic parallelism is also very powerful while preventing all sorts of nasty bugs.
I won't pretend to know all those words :P I just think the article's proposal has some merit and we should consider it. >
What conventions? Its not realistic to assume the world will change to fit your views of software :p
> At least in C/C++ it doesn't.
Sure does; it adds more build targets, gets you to maintain shared code across executables, and plan deployment for multiple executables instead of one. Thats all before even coding the support for that.
> Why not?
Because these depend on shared memory and ownership transfer for performance; you'll drastically drop performance just for the sake of isolation.
> No reason for it to stay that way.
I will literally stop using the web if pages can spawn processes :)
> I won't pretend to know all those words :P
Hehe, basically immutability makes it so nobody can mutate data, thus making it safe to be shared across threads. Uniqueness will transfer ownership such that only one thread has references to a mutable piece of memory at any time. Deterministic parallelism means its impossible to have race conditions or deadlocks.
> I just think the article's proposal has some merit and we should consider it.
Agreed, I'm still having a hard time seeing it however :p
If OSs change again, sure, the way we program will change again.
At the moment, using processes is too resource intensive, that's exactly why we have threads in the first place. And I don't understand what benefit you are suggesting using processes over threads would have.
Perhaps, but only having a complex solution that handles 100% of use cases is less desirable than having a simple one that handles 80% of the most common use cases PLUS the ability to go deeper and use the complex method (if, and only if, it's necessary...)
Still, there's value in unification and generality, at least when the result is simple, even if that covers only 80% of cases.
For example you generally run a lot more composable tasks than actual threads and syncs. You get a general, and powerful abstraction for the general case yet threads are still there for more exotic needs.
Erlang's supervisor behaviours are basically that. They add some more stuff (the actual supervision) but fundamentally every process in a supervision tree will necessarily manage and outlive all its children processes.
It solves the issue quite directly.
> There's no risk of concurrent processes unexpectedly changing state (the real problem Trio addresses, or at least reduces the blast radius of) because there is no shared state.
That doesn't seem to have any relevance to Trio's purpose, a low-level spawn has this exact same guarantee and yet is pointed to as a problematic primitive right in the introduction. The essay says literally nothing about unexpected state changes. In fact the word "state" appears nowhere in the essay. The essay is about control flows and their structuring (or lack thereof).
That's what was discussed in the example about file handles. The problem isn't bad concurrent control flow itself, the problem is that bad concurrent control flow enables unpredictable (or difficult to predict) changes in the apparent expected state all over your program. That's the same situation as with 'goto': spaghetti jumps are bad largely because they make it hard to think about "what is true when the flow of execution gets here here" at a given point in the program.
Haskell's async[1] solution is very nice too, with the exception restarting processes and fault tolerance isn't really there yet. On the other hand, in Haskell there's stuff like STM, which makes atomic updates to shared memory easy.
At the lower level side of things Rust's model is very nice too. You can statically verify that you don't have certain classes of bugs.
I'd be interested in implementing this in Go and throwing a web server implementation at it to see if it makes more sense. Though maybe that's too simple a use-case for it.
It'd be interesting to see how to implement the error propagation using channels, without having any control over the goroutine passed in. If the goroutine panics, how to capture that and pass it back to the nursery?