How do I cancel non-cancelable async operations?

How do I cancel non-cancelable async operations?

Rate This
  • Comments 32

This is a question I hear relatively frequently:

“I have an async operation that’s not cancelable.  How do I cancel it?”

The construction of the question often makes me chuckle, but I understand and appreciate what’s really being asked.  The developer typically isn’t asking how to cancel the operation itself (if they are asking that, the answer is simple: it’s not cancelable!), but rather they’re asking how to allow their program’s execution to continue upon a cancellation request even if the operation being waited on hasn’t completed yet.  That’s a different ball game.

When someone talks about canceling async operations, they typically mean one of three things:

  1. Requesting that the async operation itself cancel.  The async operation may still run for some period of time after the cancellation request comes in, either because the operation’s implementation doesn’t respect cancellation, or because the implementation isn’t very aggressive about noticing and responding to such requests, or because there’s cleanup work that needs to be done even after a cancellation request is observed, or because the operation just isn’t at a good place in its execution to be canceled.
  2. Requesting that the code waiting for the async operation to complete stop waiting.  This has nothing to do with canceling the operation itself, and in fact the operation may still execute for quite some time.  This is entirely about changing the control flow of the program such that code (which was waiting for the operation to complete before going on to do other things) stops waiting and progresses to do those other things even though the operation hasn’t completed.
  3. Both #1 and #2.  Request the async operation to cancel, but also cancel the wait on the async operation so that we may continue running sooner than the async operation might complete.

In .NET, #1 is enabled by passing a CancellationToken to the async operation in question.  For example, here I’m passing a token to a Stream operation:

FileStream fs = …;
byte [] b = …;
CancellationToken token = …;
await fs.ReadAsync(b, 0, b.Length, token);

When the token has cancellation requested, the ReadAsync operation in flight may observe that request and cancel its processing before it otherwise would have completed.  By design, the Task returned by ReadAsync won’t complete until all processing has quiesced one way or another, and the system won’t continue to execute any code after the await until it does; this is to ensure that when the await completes, you know there’s no relevant work still in flight, that you can safely manipulate any data (like the byte[] buffer) that was provided to the async operation, etc.

Ok, so what about #2… is it possible to implement #2 in .NET?  Of course… but we didn’t make it super easy.  For example, you might expect an overload of ConfigureAwait that accepts a CancellationToken, e.g.

FileStream fs = …;
byte [] b = …;
CancellationToken token = …;
await fs.ReadAsync(b, 0, b.Length).ConfigureAwait(token); // overload not in .NET 4.5

Such an overload would allow the await itself to be canceled and execution to continue after the await even if the async operation on the stream was still in flight.  But there’s the rub… this can lead to unreliability.  What if the async operation eventually completes and returns an object that should be disposed of or otherwise acted upon?  What if the async operation fails with a critical exception that gets ignored?  What if the async operation is still manipulating reference arguments provided to it? And so on.  That’s not to say a developer couldn’t cope with some such issues, e.g.

FileStream fs = …;
byte [] b = …;
CancellationToken token = …;
Task op = fs.ReadAsync(b, 0, b.Length);
try
{

    await op.ConfigureAwait(token); // overload not in .NET 4.5
}
catch(OperationCanceledException)
{
    op.ContinueWith(t => /* handle eventual completion */);
    … // whatever you want to do in the case of cancellation, but
      // be very careful if you want to use the byte[], which
      // could be modified concurrently by the async operation
      // still in flight…
}

but doing so is non-trivial.  As such, for better or worse, in .NET 4.5 such an overload of ConfigureAwait doesn’t exist, out of concern that it would become a crutch too quickly used without thinking through the ramifications, which are subtle. 

Of course, that doesn’t prevent you from implementing such functionality yourself if you believe it’s the right thing for your needs.  In fact, you can implement this #2 functionality with just a few lines of code.  Here’s one approach:

public static async Task<T> WithCancellation<T>(
    this Task<T> task, CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<bool>();
    using(cancellationToken.Register(
                s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
       
if (task != await Task.WhenAny(task, tcs.Task))
            throw new OperationCanceledException(cancellationToken);
    return await task;
}

Here we’re using a Task.WhenAny to wait for either the task to complete or for a cancellation request to arrive (which we do by creating another task that will complete when cancellation is requested).  With that function, I now can achieve #2 as outlined previously (subject to the same caveats), e.g.

FileStream fs = …;
byte [] b = …;
CancellationToken token = …;
Task op = fs.ReadAsync(b, 0, b.Length);
try
{

    await op.WithCancellation(token);
}
catch(OperationCanceledException)
{
    op.ContinueWith(t => /* handle eventual completion */);
    … // whatever you want to do in the case of cancellation
}

Of course, once I can do #1 and #2, doing #3 is straightforward, since it’s just a combination of the other two (passing the CancellationToken to the operation in addition to passing it to a WithCancellation-like function), e.g.

FileStream fs = …;
byte [] b = …;
CancellationToken token = …;
Task op = fs.ReadAsync(b, 0, b.Length, token);
try
{

    await op.WithCancellation(token);
}
catch(OperationCanceledException)
{
    if (!op.IsCompleted)
        op.ContinueWith(t => /* handle eventual completion */);
    … // whatever you want to do in the case of cancellation
}

So, can you cancel non-cancelable operations? No.  Can you cancel waits on non-cancelable operations?  Sure… just be very careful when you do.

Leave a Comment
  • Please add 1 and 7 and type the answer here:
  • Post
  • Bnaya, you could do that.  Though you could also just take advantage of the timer support built into CancellationTokenSource, e.g.

       await task.WithCancellation(new CancellationTokenSource(timeout).Token);

  • Thanks for this excellent post. But shouldn`t you use ConfigureAwait(false) (Task.WhenAny + return statement) to prevent deadlocks when UI-Code Waits for this methdod (e.x. task.WithCancellation().Wait() would deadlock)

    public static async Task<T> WithCancellation<T>(

       this Task<T> task, CancellationToken cancellationToken)

    {

       var tcs = new TaskCompletionSource<bool>();

       using(cancellationToken.Register(

                   s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))

           if (task != await Task.WhenAny(task, tcs.Task).ConfigureAwait(false))

               throw new OperationCanceledException(cancellationToken);

       return await task.ConfigureAwait(false);

    }

  • David, yes, ideally ConfigureAwait(false) should be used on all awaits in libraries that don't have a particular thread-affinity requirement (avoiding potential deadlocks is one reason for doing this; performance is another).  I just often omit doing so in blog snippets like this from a desire to keep things as concise as possible for pedagogical purposes (and maybe from a little laziness ;)

  • Excellent read Stephen. It's so to the point, clearing one more async confusion people have these days.

    Much appreciated.

    - ngm

  • @ngm: Thanks! I'm glad you found it helpful.

  • Hi Stephen,

    Great post. This has answered half of the solution I am looking for. I have the following,

    var client = await tcpListener.AcceptTcpClientAsync().WithCancellation(cancellationToken);

    Accept(client, cancellationToken);

    where Accept is itself an "async void Accept(...)" as I don't actually want to wait for the interaction with the client to finish before I accept the next client. How would you suggest clearing up the resources used within the Accept() method in this case? ie, closing/disposing of the TcpClient and any stream's that are used.

    Thanks,

    Cain

  • @Cain: I'm glad you found the post helpful.  As to your question, my suggestion: the Accept method itself should internally clean up any resources it creates, e.g. if it opens a stream, it should close it.  For resources created prior to the Accept method's invocation, you have two options: clean up those resources inside of Accept (in which case you're in effect transferring ownership of those resources from the caller to the Accept method, and the caller should not use those resources after doing so), or you should make Accept into a Task-returning method, and use a continuation off of the returned Task to clean up those resources (e.g. with ContinueWith).  From a compositional perspective, I'd personally opt for the latter, unless I found a really good reason for not doing so.  I'm not a fan of using void-returning async methods in libraries; I try to reserve their use only for event handlers and similar situations.

  • Thanks Stephen.

    One thing that still hasn't clicked with me is how do I ensure that the tasks that are created (the Accept(...) method) are cancelled correctly? I am not waiting on these tasks within my main function.

    Should I be keeping a list of the tasks and then checking for IsCancellationRequested and waiting for them to cancel? I want to be able to close the TcpClient connections before stopping the main listener?

    Thanks,

    Cain

  • @Cain: If you need to ensure that all of the async operations have completed before you tear down some resources, then yes, you would want to wait for those tasks to complete.  You might consider creating a simple data structure to which you add Tasks and from which tasks are automatically removed when they complete, e.g. when adding a task, you'd both add it to a concurrent dictionary and then also use ContinueWith to hook up a continuation that will remove it from the dictionary when it's done.  Then your main routine can just wait for all outstanding tasks to complete by getting the list from the dictionary.

  • Hi Stephen, thanks for great post.

    Are you aware of any tcp server sample  similar to www.sadev.co.za/.../tcp-servers-net-scenario-4-non-blocking-servers-net-45 but applying this approach to gracefully stop listening?

  • I have spent an enormous amount of time trying to riddle this out.  At the Visual Studio Blog:

    blogs.msdn.com/.../9635790.aspx

    It gives an example for "the second example demonstrates using the callback facility on CancellationToken for when polling is not an option."

    It looks like it will work, but I decided I needed more details, so I ended up here. I am definitely doing your Task 2. I don't care what the database does in the future, but I want the thread/task that made the call, to die without waiting for the answer that may or may not come. I am making database calls and if my main Windows Service timer says they have run too long, I want to kill the thread/task they are operating on. As far as the database is concerned, the machine that made the SQL call must have shut down or gotten disconnected. You can test this by sending a query string of "waitfor delay '00:00:15'" for a to act like a long running stored procedure (query, no rollback involved. Project includes query from AS-400 with unpredictable failure characteristics.).

    Now your example here, conveniently uses "fs.ReadAsync".  To make it short and simple, I think that a database read is a far more common situation where this could happen.

    I am doing a brute force search on the web for code I can use. I can find code like this, but, like this, I just cannot get a couple other parts assembled. Nice code, but my teeth are breaking on your bones. I think I'm tired.

  • I may have asked the wrong question (I'm trashed thrashing around this answer)... Could you give me a little help using "#2 functionality". OK, that is an extension. Ah, your code needs ,Net 4.5, but I am in VS 2010. I can fix that. I'll have VS 2012 Monday morning. Could you give me a link to any page where that extension or method is used. It would be great help. That way I can be sure I am marshaling all the parts correctly

    Yah, my previous post describes pretty well what I am doing. Making a call to a database on a thread and giving up after a certain amount of time. I just want to log it and have the task vaporize.

    Thanks for any help.

  • Hi Stephen.

    In the last catch block you are checking the IsCompleted event but if an Task is canceled it is also completed automatically and therefore would never execute a ContinueWith.

    Here a cite from the Task-based Asynchronous Pattern pdf file from... aehm ... you ;-)

    "...If the token has cancellation requested and the asynchronous operation is able to respect that request,

    the returned task will end in the TaskStatus.Canceled state; there will be no available Result and no

    Exception. The Canceled state is considered to be a final, or completed, state for a task, along with the

    Faulted and RanToCompletion states. Thus, a task in the Canceled state will have its IsCompleted

    property returning true. When a task completes in the Canceled state, any continuations registered

    with the task will be scheduled or executed, unless such continuations opted out at the time they were

    created, through use of specific TaskContinuationOptions (e.g.

    TaskContinuationOptions.NotOnCanceled). Any code asynchronously waiting for a canceled task

    through use of language features will continue execution and receive an OperationCanceledException

    (or a type derived from it). Any code blocked synchronously waiting on the task (through methods like

    Wait or WaitAll) will similarly continue execution with an exception..."

    Special attention to: "a task in the Canceled state will have its IsCompleted property returning true"

    Thanks

  • @Mike: I think you're misreading the code... in that catch block, it's not the Task returned from WithCancellation that's having IsCompleted/ContinueWith called on it, but rather the original Task returned from ReadAsync.  If that Task has already completed (due to cancellation or otherwise), then the operation really is no longer running and there's no need to schedule a continuation.  The point of that ContinueWith is just to allow for dealing with the eventual completion (again, for whatever reason) if the task isn't already completed because we got here due to the WithCancellation task completing, not the original one.

  • @Miguelito: I'm not sure what you're asking for regarding #2, as most of this post demonstrates how to apply #2... that's what the WithCancellation method is implementing.

Page 2 of 3 (32 items) 123