[1] https://vorpus.org/blog/notes-on-structured-concurrency-or-g...
[1] https://vorpus.org/blog/notes-on-structured-concurrency-or-g...
I am deeply aware of njs’s notes on structured concurrency & have been working on a post engaging with it. This post was not about structured concurrency & so it wasn’t relevant here.
> His claim is that structured concurrency is the only way to make Ctrl+C handling work in a way how users naively expect it to work.
> And it’s easy to see why: If the exceptions are propagated up the call tree, freely crossing the thread boundaries, the KeyboardInterrupt exception is eventually going to reach the top of the call three (main function) and exit cleanly.
> However, there’s a hidden race condition involved: Imagine two threads in a scope, one gets KeyboardInterrupt and the other one raises an unrelated exception. If the latter exception bubbles up to the parent thread faster it will cancel the thread that’s processing KeyboardInterrupt and Ctrl+C would get lost.
I wonder if progress has been made since 2019.
I don't think there's a race condition in what you've described. Trio and that article is about single threaded async applications, but it's true that it could apply to threads in principle. But, in that case, I would expect cancellation to only be propagated at specific points, much like Trio's checkpoints [1]. You can see exactly that concept applied to threads in C#'s CancellationToken object [2]. You can only cancel a C# Task (which can be running in another thread or asynchronously in this thread) by using one of those, and the cancellation only takes effect when the Task checks it (e.g. by passing the token to a suitable async routine, or by explicitly calling CancellationToken.ThrowIfCancellationRequested()).
So, in the example you gave, there's two possibilities: (1) A race happens because an exception is thrown right after a checkpoint (i.e., cancellation check) and the cancel exception gets thrown at that checkpoint - but that's not really a race (except in your application logic) because that's what you asked for. Or, (2), you get to the point of throwing an exception in one thread after cancellation has been requested from another thread (but before you got a chance to check it) - but in that case the exception will propagate to the top of the thread's call stack (if not otherwise caught) and out of the thread into your nursery-like concept. So you'll end up with a multi-exception containing the KeyboardInterrupt and the other thread's exception.
In the second case, there's a "race" in that you requested cancellation of a task but it finished before it had a chance to be cancelled. But that's always a risk, even with single-threaded async applications. I think it's just fundamental to the concept of cooperative cancellation.
[1] https://trio.readthedocs.io/en/stable/reference-core.html#ch...
[2] https://learn.microsoft.com/en-us/dotnet/api/system.threadin...
(Aside: Personally, when using Trio in any application, I run it in a secondary thread, and I save the main thread for just waiting for KeyboardInterrupt (or for the secondary thread to finish naturally). That way, I can propagate the cancellation to the right bit of my application, which allows shutdown in an orderly fashion. I view Trio's default KeyboardInterrupt handling as just a decent attempt for those that aren't writing a serious enough application to need really reliable handling.)
> The popular concurrency primitives – go statements, thread spawning functions, callbacks, futures, promises, ... they're all variants on goto, in theory and in practice. [...] Therefore, like goto, they have no place in a modern high-level language.
That's a really long and abrasive article ("Everyone else's stuff is GOTO, but mine isn't") to read just to get to "Wait for your return values before moving on".
No mention of any kind of synchronisation primitives, liveness guarantees, determinism, channels, transactions...