-
-
Notifications
You must be signed in to change notification settings - Fork 357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Atomic wait semantics lead to continued processing after cancellation #270
Comments
You do bring up some good points. This would, however, be a significant breaking change. |
This has the same behavior as |
My previous argument was based on the effect when not doing an atomic wait. I just needed to actually do an atomic wait for real and found some additional concerns. When doing an atomic wait, the
The following code demonstrates what I mean: public async Task Example(CancellationToken ct = default)
{
try
{
using(_asyncLock.Lock(new CancellationToken(true)).ConfigureAwait(false))
{
await SaveAsync(results, ct);
}
}
catch (OperationCancelledException)
{
// This will catch both the expected exception from the call to Lock(), and the actual cancellation
// from the call to SaveAsync(). To avoid this, one would need to try/catch just the Lock() while
// saving the disposable into a variable to put into a using after the try/catch.
}
} I created an extension method to public async Task Example(CancellationToken ct = default)
{
using(_asyncLock.LockNow(out var held))
{
if (!held)
return;
await SaveAsync(results, ct);
}
} |
I understand and agree with your concerns about "Try" being ambiguous. When I first saw the approach of using already canceled tokens to do atomic waits, I thought it was clever. However, I've since found that it leads to unexpected behavior after cancellation. I recommend that an alternative approach be found.
Explanation:
The typical pattern when using cancellation tokens is that it is not necessary to constantly check the token using
ThrowIfCancellationRequested()
because any method you pass the token to will do that for you. However, the atomic wait semantics break this. For example, in a method that acquires a lock, does some costly synchronous work then saves the result this can lead to extra processing and side effects even after the token has been canceled. The method below demonstrates this. If the token is already canceled by the time the method is entered, then assuming the lock can be acquired without waiting, it will be, and the costly processing and side effects will occur before theSaveAsync
throws anOperationCancelled
exception.This behavior is contrary to the expectations and practices developers have built up from other async APIs. To handle this correctly, the developer must be conscious of which methods support atomic wait and explicitly put a
ThrowIfCancellationRequested()
before calling them. Given that acquiring a lock is a relatively costly operation, this is always the desired behavior.Another way to see that the atomic waits approach has a problem is that it doesn't generalize. Imagine if all async APIs followed the pattern that if they could process immediately without delay, then they would continue despite receiving a canceled token. This would frequently lead to execution continuing after the operation was canceled until a delayed operation was hit.
The text was updated successfully, but these errors were encountered: