Adding structured concurrency to JavaScript
11 points by bakkot
11 points by bakkot
I like that post but I think it's basically wrong in claiming that structured concurrency is impossible with current async/await semantics. Specifically, the claim
Instead of waiting around for async operations to complete that have no bearing on the outcome of a computation, a structurally concurrent system will return from those functions immediately the moment they are no longer necessary.
is wrong: a structurally concurrent system should cancel the outstanding functions immediately, but it must not return until they've completed cleanup. Otherwise you've violated the core tenant of structured concurrency, that functions cannot outlive the invoking scope. And indeed that's how it works in Python; that's why you need to async with your TaskGroup, so that it can pause at that point. (Also Swift and Java, albeit via slightly different mechanisms.)
If you allow async cleanup, then you necessarily are in the position that callers are forced to wait on callees to finish working. You can (and should) have some mechanism of eagerly indicating to callees that they should stop whatever they were doing and instead clean up, but you are still forced to wait for cleanup to finish once cancelation has been signaled, and you have no guarantee that the callee is actually going to respect this signal.
In other words, at least in the absence of a much stronger type system than in any mainstream language, structured concurrency is inherently cooperative. It's best when that cooperation is as seamless as possible (as opposed to, say, requiring that all async tasks take and respect a signal argument and thread it to their subtasks), but that is an ergonomics thing, not a core property.
I think I was getting hung up on controller.wrap(promise) alternative, and had to mentally rewrite it in terms of allSettled to get my head around it.
That helped me see the distinction a bit better: for the "known batch of promises" case, allSettled gets surprisingly close, but the broader thing being aimed at here is scope-owned shutdown rather than just result aggregation (if I read that correctly)?
Also, I learned await using existed, so this was educational on multiple fronts.
This is a cool set of ideas. I agree that there needs to be a better way of combining using and abort signals, and just generally tighter integration of Disposable with existing browser APIs.
I've tried in my own projects an approach where I've mostly avoided abort signals and concentrated on using Disposable/AsyncDisposable everywhere, but ultimately this fails as soon as you hit promises. Because promises don't have any native cancellation system, if you want to dispose of a promise, you need some sort of out-of-band cancellation mechanism, and while you can put something together with disposable wrappers, ultimately you're just reinventing abort signals. So any system that links abort signals and disposables seems sensible to me.
I think concentrating on the Promise.map/.race methods makes a lot of sense. In my experience, these are currently difficult functions to use because of the numerous cancellation hazards that can occur, especially .race because the whole point of that function is that most of the promises passed in should not run to completion, and yet the naive approach means that they all will. I agree that adding Promise.raceButWithABetterAPI probably isn't the best approach, but I think thinking about what the average .race call would look like is a good test of whether new APIs are ergonomic to use.