A glance at the past
Back in the old .NET days we used a BackgroundWorker instance to run asynchronous and long-running operation.
We had the ability to cancel these operations by calling the CancelAsync which sets the CancellationPending flag to true.
private void BackgroundLongRunningTask(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = (BackgroundWorker)sender;
for (int i = 1; i <= 10000; i++)
{
if (worker.CancellationPending == true)
{
e.Cancel = true;
break;
}
// Do something
}
}
It is not the recommended way to use asynchronous and long-running operations anymore. But most of the concepts that were in used back then made it to present days in the form of Tasks and CancellationTokens.
Using Tasks
The Task class represent an asynchronous operation. The vanilla class does not return a value but the generic version does.
You can use Task directly or use the async await pattern to simplify the code. For this post we will use the latter.
The sample application
In order to support this post I have created a code sample and posted it on Github. It is a .NET Core 2 Console Application which should run everywhere.
Let’s create a simple long running operation :
/// <summary>
/// Compute a value for a long time.
/// </summary>
/// <returns>The value computed.</returns>
/// <param name="loop">Number of iterations to do.</param>
private static Task<decimal> LongRunningOperation(int loop)
{
// Start a task and return it
return Task.Run(() =>
{
decimal result = 0;
// Loop for a defined number of iterations
for (int i = 0; i < loop; i++)
{
// Do something that takes times like a Thread.Sleep in .NET Core 2.
Thread.Sleep(10);
result += i;
}
return result;
});
}
Here we used a Thread.Sleep call to simulate a long running operation but of course, if you actually use a Thread.Sleep in production you probably have a problem in your code.
Basic call
A very simple way to call the LongRunningOperation method and get its results is by awaiting it. To be able to await it we need of course to make our calling method async.
public static async Task ExecuteTaskAsync()
{
Console.WriteLine(nameof(ExecuteTaskAsync));
Console.WriteLine("Result {0}", await LongRunningOperation(100));
Console.WriteLine("Press enter to continue");
Console.ReadLine();
}
Making our code cancellable
If for any reason, the long running operation takes too long to execute or we do not need its result anymore, we might want to cancel it.
In order to do that we need to pass a CancellationToken to the long running method.
/// <summary>
/// Compute a value for a long time.
/// </summary>
/// <returns>The value computed.</returns>
/// <param name="loop">Number of iterations to do.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private static Task<decimal> LongRunningCancellableOperation(int loop, CancellationToken cancellationToken)
{
Task<decimal> task = null;
// Start a task and return it
task = Task.Run(() =>
{
decimal result = 0;
// Loop for a defined number of iterations
for (int i = 0; i < loop; i++)
{
// Check if a cancellation is requested, if yes,
// throw a TaskCanceledException.
if (cancellationToken.IsCancellationRequested)
throw new TaskCanceledException(task);
// Do something that takes times like a Thread.Sleep in .NET Core 2.
Thread.Sleep(10);
result += i;
}
return result;
});
return task;
}
Checking if the method needs to stop is done by reading the IsCancellationRequested property. It is also possible to use the ThrowIfCancellationRequested method which will throw an OperationCanceledException.
I tend to prefer throwing the exception myself since I can pass the task that was canceled in the TaskCanceledException’s constructor, giving more handling options for the method’s caller.
The relationship with BackgroundWorker’s way of doing things is pretty obvious, the main difference is that we now throw an Exception instead of just exiting the method.
Cancel with a timeout
Manipulating a CancellationToken state is done through the CancellationTokenSource instance that created it. It is able to handle a timeout by specifiying its value at construction time. Therefore, calling the LongRunningCancellableOperation method with a timeout is done this way :
public static async Task ExecuteTaskWithTimeoutAsync(TimeSpan timeSpan)
{
Console.WriteLine(nameof(ExecuteTaskWithTimeoutAsync));
using (var cancellationTokenSource = new CancellationTokenSource(timeSpan))
{
try
{
var result = await LongRunningCancellableOperation(500, cancellationTokenSource.Token);
Console.WriteLine("Result {0}", result);
}
catch (TaskCanceledException)
{
Console.WriteLine("Task was cancelled");
}
}
Console.WriteLine("Press enter to continue");
Console.ReadLine();
}
Manually cancel a task
Often, we need to manually cancel a task. This is done through the CancellationTokenSource.Cancel method.
public static async Task ExecuteManuallyCancellableTaskAsync()
{
Console.WriteLine(nameof(ExecuteManuallyCancellableTaskAsync));
using (var cancellationTokenSource = new CancellationTokenSource())
{
// Creating a task to listen to keyboard key press
var keyBoardTask = Task.Run(() =>
{
Console.WriteLine("Press enter to cancel");
Console.ReadKey();
// Cancel the task
cancellationTokenSource.Cancel();
});
try
{
var longRunningTask = LongRunningCancellableOperation(500, cancellationTokenSource.Token);
var result = await longRunningTask;
Console.WriteLine("Result {0}", result);
Console.WriteLine("Press enter to continue");
}
catch (TaskCanceledException)
{
Console.WriteLine("Task was cancelled");
}
await keyBoardTask;
}
}
Since this sample is a console application, we need to start a new task to cancel the long running operation when the keyboard is pressed. In a GUI based application such as a Xamarin Native or a Xamarin Forms application, buttons or navigations events are most likely to serve as the cancellation trigger.
Cancel a non cancellable task
Sometimes, we use code that does not expose a CancellationToken, so there is no way cancel it. Still, there are cases when we want the method to return immediately. In this scenario, the CancellationToken.Register method is our friend.
Let’s make of first LongRunningOperation method “cancellable” by creating a wrapper around it :
private static async Task<decimal> LongRunningOperationWithCancellationTokenAsync(int loop, CancellationToken cancellationToken)
{
// We create a TaskCompletionSource of decimal
var taskCompletionSource = new TaskCompletionSource<decimal>();
// Registering a lambda into the cancellationToken
cancellationToken.Register(() =>
{
// We received a cancellation message, cancel the TaskCompletionSource.Task
taskCompletionSource.TrySetCanceled();
});
var task = LongRunningOperation(loop);
// Wait for the first task to finish among the two
var completedTask = await Task.WhenAny(task, taskCompletionSource.Task);
// If the completed task is our long running operation we set its result.
if (completedTask == task)
{
// Extract the result, the task is finished and the await will return immediately
var result = await task;
// Set the taskCompletionSource result
taskCompletionSource.TrySetResult(result);
}
// Return the result of the TaskCompletionSource.Task
return await taskCompletionSource.Task;
}
Please take a bit of your time to read the comments in the code.
Here we use a TaskCompletionSource to wrap the underlying LongRunningOperation. They support cancellation, which is exactly what we need in order to transform a non-cancellable operation into a cancellable one. There are, of course, other ways to achieve the same thing. But I like this one, it is clean and async await friendly.
Please note that we do not actually cancel the underlying method. We just enable our own code to return earlier without waiting for it to end.
As suggested in the comments below by Oliver Münzberg, you can actually modify the previous code and remove the last 12 lines which will then become :
private static async Task<decimal> LongRunningOperationWithCancellationTokenAsync(int loop, CancellationToken cancellationToken)
{
// We create a TaskCompletionSource of decimal
var taskCompletionSource = new TaskCompletionSource<decimal>();
// Registering a lambda into the cancellationToken
cancellationToken.Register(() =>
{
// We received a cancellation message, cancel the TaskCompletionSource.Task
taskCompletionSource.TrySetCanceled();
});
var task = LongRunningOperation(loop);
// Wait for the first task to finish among the two
var completedTask = await Task.WhenAny(task, taskCompletionSource.Task);
return await completedTask;
}
Calling the LongRunningOperationWithCancellationTokenAsync method is now done exactly as previously :
public static async Task CancelANonCancellableTaskAsync()
{
Console.WriteLine(nameof(CancelANonCancellableTaskAsync));
using (var cancellationTokenSource = new CancellationTokenSource())
{
// Listening to key press to cancel
var keyBoardTask = Task.Run(() =>
{
Console.WriteLine("Press enter to cancel");
Console.ReadKey();
// Sending the cancellation message
cancellationTokenSource.Cancel();
});
try
{
// Running the long running task
var longRunningTask = LongRunningOperationWithCancellationTokenAsync(100, cancellationTokenSource.Token);
var result = await longRunningTask;
Console.WriteLine("Result {0}", result);
Console.WriteLine("Press enter to continue");
}
catch (TaskCanceledException)
{
Console.WriteLine("Task was cancelled");
}
await keyBoardTask;
}
}
Conclusion
Most of the time we do not need to write custom cancellable tasks as we just need to consume existing API. But it is always good to know how it is working under the hood. And now, if you encounter a non-cancellable asynchronous code, you know what to do !
As always, I will be more than happy to answer your questions in the comments and please take a look at the Github repository for this post.
Comments