6 ms·
I thought the title was kinda clickbaity, but it turned out to be a great article. Also the comparison to goto really effectively conveyed the point he was tryi
by m0meni 8y ago
I thought the title was kinda clickbaity, but it turned out to be a great article. Also the comparison to goto really effectively conveyed the point he was trying to make. I have two questions though:
* 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?
- jeremiep 8y agoI don't think its a superior paradigm, just a different one. 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.
- scott_s 8y ago> 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! 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.
- jeremiep 8y agoThen what is the difference between the nursery escape and joining a thread or extracting the value of a promise? Both of these will bubble any error back into the dispatch thread. Seems like we're just layering indirections (I wouldn't call that an abstraction) for little added value over the existing constructs.
- scott_s 8y agoThe difference is that you can look at function, see it takes no nurseries as parameters, and know it does not spawn any background tasks. And if it does take nurseries, then you know it's likely that it does.
- jeremiep 8y agoWhats the difference between taking a nursery and returning a promise? Both put the context in the signature, but the later is actually composable.
- scott_s 8y agoWhen I call a function, I have no way of knowing if there are any unhandled promises within it. The difference is the restriction: with nurseries, background tasks can only outlive function calls if the calling function allows them to.
- jeremiep 8y agoI get compile-time warnings or runtime errors when I create unhandled promises. What more is needed?
- scott_s 8y agoNeeded is a strong word, but what this provides are guarantees when reading the code. Much like you know that control flow will come back to the function you're reading after calling another function. We didn't need to know that control flow will resume in the calling function, but it turned out to be very useful.
- dingo_bat 8y ago> 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. If your background operation has those characteristics, maybe it is better to spawn a new process for it. Why a thread?
- lowmagnet 8y agoI use concurrency in the form of an error group combined with an injected context and a way of propagating the cancel via that context to an exec if I we're going that route. It's a best of both worlds solution IMO.
- jeremiep 8y agoA process has much higher overhead (both in time and space), more complex communications and is not always possible in the first place. It also adds complexity to the build pipeline. These quickly adds up to being not worth it over a task or 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.
- dingo_bat 8y ago> A process has much higher overhead (both in time and space), more complex communications and is not always possible in the first place. 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. >
- coldtea 8y ago>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! 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...)
- jeremiep 8y agoAgreed, I'd rather have a few simple constructs, especially if they compose. 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.
- masklinn 8y ago> * Does anything else like this currently exist (other than the Trio library he mentions), which shows that it's a superior paradigm in practice? 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.
- zbentley 8y agoSure, but Erlang solves this problem indirectly. 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.
- masklinn 8y ago> Sure, but Erlang solves this problem indirectly. 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).
- zbentley 8y agoThe entire reason why trio is proposed as useful is because of bad control flow leading to unpredictable state. 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.
- marcus_holmes 8y agoSame. I kinda eye-rolled at the title, but got hooked by the article. 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?
- leshow 8y agoErlang's concurrency is probably the most similar (greenthreading, channels, messages) and best out there. Errors are handled nicely via supervisor; you can produce a long running fault tolerant system. 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. 1: https://hackage.haskell.org/package/async https://hackage.haskell.org/package/async
- rumcajz 8y agoI've written a similar thing in C (there's a link in the article), here's the intro: http://libdill.org/structured-concurrency.html http://libdill.org/structured-concurrency.html