[UWP] Cancel an async method while awaiting it

What happens when you await a task and want to cancel it? What happens if the caller is set to null if the task is still running? Is the task cancelled or does it keep running?

cancel

A method marked with async is awaitable like this:

public async Task Foo() { ... }
await Foo();

Because it is awaited, Foo is scheduled on a separate thread and when it returns, the result (in this case void or an exception) is unwrapped and returned to the caller. We have no control over the Task itself.

What if we want to Cancel the Task?

If work is scheduled with Task.Run a cancellation token can be passed to the Run method like this:

var cts = new CancellationTokenSource();
var token = cts.Token;

Task.Run(() => {
    while (!token.IsCancellationRequested)
    {

    }
}, token);

Let’s write the Action from above in a separate method:

private async Task Foo()
{
    while (true) ???
    {

    }
}

While we can call and cancel Foo like this:

var task = Foo2().AsAsyncAction();
task.Cancel();

the only thing that would be cancelled, is the async operation itself. To cancel the action, we need access to the CancellationToken so we can respond to IsCancellationRequested:

private async Task Foo(CancellationToken token)
{
    while (!token.IsCancellationRequested)
    {

    }
}

Now we can pass the token when we call Foo. That way Foo can take action to cancel if needed.

What if the caller is set to null?

If the caller of an async method is set null, the Task keeps running. Let’s check this behavior with a small Universal Windows demo Project.

Assume a UWP with a MainPage.xaml.cs that instantiates class Caller that instantiates class Worker

MainPage.xaml.cs

private Caller _caller;

private async void Button_Click(object sender, RoutedEventArgs e)
{
    _caller = new Caller();
    await _caller.Start();
}

Caller.cs

public async Task Start()
{
    var worker = new Worker();
    await worker.Foo();
}

Worker.cs

public async Task Foo()
{
    while (true)
    {
        await Task.Delay(2000);
    }
}

Now let’s set the Caller to null while a task is running:

private void Button_Click_2(object sender, RoutedEventArgs e)
{
    _caller = null;
}

In the above example, if the MainPage sets the instance of Caller to null, the task keeps running. That might happen when you navigate away from the MainPage. To prevent orphan Tasks, we need to cancel them.

Just like in the previous example, pass CancellationToken to the working method like this:

Worker.cs:

public async Task Foo(CancellationToken cancel)
{
    while (!cancel.IsCancellationRequested)
    {
        // after cancelling, this code will not be executed anymore
        // but when setting the caller to null, this code keeps executing.
        // test it by putting a breakpoint on the next line
        await Task.Delay(2000);
    }
}

Create a Cancel method in the Caller:

Caller.cs:

public void Cancel()
{
    _cancel.Cancel();
}

and now you can safely cancel and set _caller to null in the MainPage:

private void Button_Click_2(object sender, RoutedEventArgs e)
{
    _caller.Cancel();
    _caller = null;
}

If you want to experiment yourself, download a demo that includes progress reporting.