Hacker News new | ask | show | jobs
by fvncc 1082 days ago
One advantage of async/await is that its easier to cancel things. For example, this leads to the design pattern where you have multiple futures and you want to select the one that finishes first and cancel the rest.

In regular threaded programming, cancellation is a bit more painful as you need to have some type of cancellation token used each time the thread waits for something. This a) is more verbose and b) can lead to bugs where you forget to implement the cancellation logic.

2 comments

>In regular threaded programming, cancellation is a bit more painful

No, it isn't. Nothing is stopping your threading library from implementing the same thing. It just turns out it is a bad idea to kill threads at random points in time because they may own things like locks. Or in the case of async await doing something that is thought to be atomic.

You're describing exactly why it's painful for threads. If you only cancel co-routines at yield points (which unless you do dark magic is the only time you can cancel them) is always safe.

A co-routine that yields in the middle of an atomic operation is an oxymoron. Anything can happen before you're scheduled again.

Well each time you use the await keyword you are saying its a safe point to exit, which is more predictable than killing at random points. Holding locks across await points is an anti-pattern, and Rust at least can give a hint if you try to do that. Async/await implementations will also generally allow you to run cleanup code on cancellation (but the exact mechanism depends on the language).

In the end, its about expressing a state machine in a more concise implicit way, which is a suitable level of abstraction for most use cases.

> One advantage of async/await is that its easier to cancel things. For example, this leads to the design pattern where you have multiple futures and you want to select the one that finishes first and cancel the rest.

> In regular threaded programming, cancellation is a bit more painful as you need to have some type of cancellation token used each time the thread waits for something. This a) is more verbose and b) can lead to bugs where you forget to implement the cancellation logic.

Yeah of course Rust just makes cancellation so easy by allowing the Futures to be dropped. What about the resources these Futures could have allocated within the context that are not just memory? You are saying it as if async/await somehow solved the whole problem of stack unwinding.

Since rust follows RAII, any and all resources allocated in the context should be deallocated when their destructor (the drop trait) is called. The unfortunate exception to this are resources which require an async call to deallocate properly. Though this can be worked around and there is work being done to fix this properly.
> Since rust follows RAII, any and all resources allocated in the context should be deallocated when their destructor (the drop trait) is called. The unfortunate exception to this are resources which require an async call to deallocate properly. Though this can be worked around and there is work being done to fix this properly.

Yeah as I said async does not, in fact, "provide easily cancellable execution patterns".

only if the resources are just used within this task. If an async function at some point in time generates another task (or even spawns a thread!) that can not be synchronously cancelled then it might outlive the destructor and thereby the async task. It's therefore nowhere near guaranteed that every Future can just be dropped to stop an action in a side-effect free fashion.
I think the comment was about async in general, not just Rust (although that's the topic of OP).

In Python, cancellation causes an exception to be injected at the await site, which allows it to clean up whatever resources it likes (even if that means making other async calls). If you use Trio or the new TaskGroup in asyncio (inspired by Trio) then an exception leaking out of one task causes the others to be cancelled, and then the task group waits for all tasks to complete (successfully, with exception, or cancelled). It's extremely nice and easy to write reliable programs.

In principle, I think many of these ideas could be applied to threaded IO. But I haven't seen it done in practice.

> In principle, I think many of these ideas could be applied to threaded IO.

That's how POSIX deferred [1] cancellation works. An uncatchable exception is thrown from blocking calls if a thread is requested to terminate. As POSIX is C centered, you can imagine that handling exceptions was never popular, but it should work fine in C++. For some reasons it wasn't added to std::thread though.

[1] there is also async cancellation, but friends do not let friends use PTHREAD_CANCEL_ASYNCHRONOUS.

> In Python, cancellation causes an exception to be injected at the await site, which allows it to clean up whatever resources it likes (even if that means making other async calls). If you use Trio or the new TaskGroup in asyncio (inspired by Trio) then an exception leaking out of one task causes the others to be cancelled, and then the task group waits for all tasks to complete (successfully, with exception, or cancelled). It's extremely nice and easy to write reliable programs.

But not in JavaScript.