2

I encountered a problem how to properly cancel an asynchronous task.

Here is some draft.

My entry point runs two asynchronous task. The first task does some 'long' work and the second one cancels it.

Entry point:

private static void Main()
{
    var ctc = new CancellationTokenSource();

    var cancellable = ExecuteLongCancellableMethod(ctc.Token);

    var cancelationTask = Task.Run(() =>
    {
        Thread.Sleep(2000);

        Console.WriteLine("[Before cancellation]");

        ctc.Cancel();
    });

    try
    {
        Task.WaitAll(cancellable, cancelationTask);
    }
    catch (Exception e)
    {
        Console.WriteLine($"An exception occurred with type {e.GetType().Name}");
    }
}

Method that returns cancel-able task:

private static Task ExecuteLongCancellableMethod(CancellationToken token)
{
    return Task.Run(() =>
    {
        token.ThrowIfCancellationRequested();

        Console.WriteLine("1st"); 
        Thread.Sleep(1000);

        Console.WriteLine("2nd");
        Thread.Sleep(1000);

        Console.WriteLine("3rd");
        Thread.Sleep(1000);

        Console.WriteLine("4th");
        Thread.Sleep(1000);

        Console.WriteLine("[Completed]");

    }, token);  
}   

My purpose is to stop writing '1st','2nd','3rd' immediately after cancellation is called. But I get following results:

1st
2nd
3rd
[Before cancellation]
4th
[Completed]

For obvious reason I didn't get an exception that throws when cancellation is requested. So I tried to rewrite method as following:

private static Task ExecuteLongCancellableAdvancedMethod(CancellationToken token)
{
    return Task.Run(() =>
    {
        var actions = new List<Action>
        {
            () => Console.WriteLine("1st"),
            () => Console.WriteLine("2nd"),
            () => Console.WriteLine("3rd"),
            () => Console.WriteLine("4th"),
            () => Console.WriteLine("[Completed]")
        };

        foreach (var action in actions)
        {
            token.ThrowIfCancellationRequested();

            action.Invoke();

            Thread.Sleep(1000);
        }

    }, token);
}

And now I got what I want:

1st
2nd
[Before cancellation]
3rd
An exception occurred with type AggregateException

But I guess creating of a collection of Action delegates and looping through it is not the most convenient way to deal with my problem.

So what's the proper way to do it? And why do I need to pass my cancellation token into Task.Run method as the second argument?

  • If the cancellation for the token had been already requested, `Task.Run` won't run the delegate at all – Jakub Dąbek Aug 29 '17 at 19:15
  • 1
    You need to explicitly call `token.ThrowIfCancellationRequested();` at every place where you want the execution to actually be cancellable. So in your first example, before every `Console.WriteLine`. – Bradley Uffner Aug 29 '17 at 19:29
  • 1
    I highly recommend that you read up on the ContinueWith method as well since you asked about the "proper" way, I think you'll want to get familiar with ContinueWith and TaskContinuationOptions.NotOnCanceled. Here's a link with more helpful info: https://social.msdn.microsoft.com/Forums/en-US/310782d8-aadf-4841-a309-23abff407d9a/cancellation-using-exception-to-control-application-flow?forum=parallelextensions – Jace Aug 29 '17 at 20:29

1 Answers1

4

The Task won't cancel its self, it is up you you to detect the cancellation request and cleanly abort your work. That's what token.ThrowIfCancellationRequested(); does.

You should place those checks throughout your code, in places where execution can be cleanly stopped, or rolled back to a safe state.

In your second example, you call it once per iteration of the loop, and it works fine. The first example only calls it once, at the very start. If the token hasn't been canceled by that point, the task will run to completion, just like you are seeing.

If you changed it to look like this, you would also see the results you expect.

return Task.Run(() =>
{
    token.ThrowIfCancellationRequested();
    Console.WriteLine("1st"); 
    Thread.Sleep(1000);

    token.ThrowIfCancellationRequested();
    Console.WriteLine("2nd");
    Thread.Sleep(1000);

    token.ThrowIfCancellationRequested();
    Console.WriteLine("3rd");
    Thread.Sleep(1000);

    token.ThrowIfCancellationRequested();
    Console.WriteLine("4th");
    Thread.Sleep(1000);

    Console.WriteLine("[Completed]");

}, token); 
Bradley Uffner
  • 16,641
  • 3
  • 39
  • 76
  • 1
    Thanks, I know how the code I wrote works, but is there any more convenient ways to cancel a task? I mean writing 'ThrowIfCancellationRequested' after every logical block makes code look awful as for me – Vladyslav Yefremov Aug 29 '17 at 19:42
  • 2
    Nope, no way that I know of. Only the developer can know where the safe places in the code are to cleanly cancel the `Task`. The code within the `Task` could leave variables and data in an unclean, unknown state, if it were to somehow cancel as soon as the cancellation is requested, and [that could be just as dangerous as `Thread.Abort`](https://stackoverflow.com/questions/1559255/whats-wrong-with-using-thread-abort). Just imagine the consequences if the `Task` where to take a lock on an object, and it was canceled before it could be released, or if the Task was dealing with unmanaged memory – Bradley Uffner Aug 29 '17 at 20:08