Daily Knowledge Drop
A new method on Task
called WaitAsync
was introduced in .NET6. This method allows for waiting on a Task for a specific period of time before throwing a timeout exception.
On the surface, this might not seem very useful, but lets look at some examples to see how this new method can be leveraged.
The issue
Long running processes
Suppose we have a long running method which returns a Task - in the below example we are simulating a download process which takes 5 seconds
await LongRunningProcessAsync();
async Task LongRunningProcessAsync()
{
Console.WriteLine($"{DateTime.Now.TimeOfDay} => Starting large download...");
for (int x = 0; x < 4; x++)
{
await Task.Delay(1000);
}
Console.WriteLine($"{DateTime.Now.TimeOfDay} => Download Complete");
}
As probably expected, the output is a follows, with 5 seconds gap between the start and completion of the "download":
06:51:29.4337633 => Starting large download...
06:51:34.4755862 => Download Complete
In the above example, the long running process is capped at 5 seconds, but in a real world situation, this would be an unknown number, dependant the size of the file downloading, network speed and availability etc.
So how can we force a cap on the download time
, and if it takes longer than the cap the process is cancelled
, and feedback can be given back to the user.
Solutions
Cancellation token
The best way to handle cancellation of the task would be by using a CancellationToken
.
// A new CancellationTokenSource is used, with a timeout of 1 second
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)))
{
try
{
// The long running method is called, and has been
// updated to accept a CancellationToken, which is supplied
await LongRunningProcessAsync(cts.Token);
}
catch (TimeoutException)
{
Console.WriteLine($"{DateTime.Now.TimeOfDay} => " +
$"Download took too long and was cancelled");
}
}
async Task LongRunningProcessAsync(CancellationToken token)
{
Console.WriteLine($"{DateTime.Now.TimeOfDay} => Starting large download...");
for (int x = 0; x < 4; x++)
{
// Every iteration, the token is checked to see if a cancellation
// was requested (which in this example would happen after 1 second)
if(token.IsCancellationRequested)
{
// If a cancellation has been requested, throw an
// exception and abort the long running process
throw new TimeoutException();
}
await Task.Delay(1000);
}
Console.WriteLine($"{DateTime.Now.TimeOfDay} => Download Complete");
}
The output is as follows:
07:07:28.2537474 => Starting large download...
07:07:29.3237966 => Download took too long and was cancelled
PROS:
- The long running process stops when the cancellation occurs
CONS:
- The method needs to change to support a cancellation token as a parameter
In this example changing the method to support the cancellation token was straightforward, and many existing third party libraries already support cancellation tokens.
But how could the same problem be solved when the method does not support a cancellation token and cannot be changed
?
Parallel tasks
One method is to start a second task which runs for a set period of time, in parallel to the long running process
. We wait for one of the tasks to finish and based on which finished first, we can handle how to proceed.
// start the process of creating cancellation token
using (var cts = new CancellationTokenSource())
{
try
{
// create a timeout delay task, which will run for 1 second
var timeout = TimeSpan.FromSeconds(1);
var timeoutTask = Task.Delay(timeout, cts.Token);
// wait for either the timeout task OR the
// long running task to finish
var firstTask = await Task.WhenAny(new[] { timeoutTask,
LongRunningProcessAsync() });
// if the first task to finish was the timeout task,
// we can throw a timeout exception as the long running
// process has now exceeded the allowed timeout
if (firstTask == timeoutTask)
{
throw new TimeoutException();
}
else
{
cts.Cancel();
}
}
catch (TimeoutException)
{
Console.WriteLine($"{DateTime.Now.TimeOfDay} => " +
$"Download took too long and was cancelled");
}
}
// This method is unable to change, so no CancellationToken can be used
async Task LongRunningProcessAsync()
{
Console.WriteLine($"{DateTime.Now.TimeOfDay} => Starting large download...");
for (int x = 0; x < 4; x++)
{
await Task.Delay(1000);
}
Console.WriteLine($"{DateTime.Now.TimeOfDay} => Download Complete");
}
Note: The above could have been done without the use of the CancellationTokenSource and use of cts.Token. However, whenever possible if a method (Task.Delay in this case) supports a cancellation token parameter, it should be used.
The output is as follows:
07:56:40.5198266 => Starting large download...
07:56:41.5520772 => Download took too long and was cancelled
07:56:44.5803565 => Download Complete
But wait - the download still completed?
Unfortunately, because the method doesn't accept a cancellation token (or provide some other way) to handle cancellations, there is no way to truly cancel it.
What the above technique does, is allow the flow of execution to continue after waiting for the task for a certain period of time. The long running process task will still run to completion in the background.
PROS:
- A long running task can be "cancelled" even without a cancellation token or any modifications to code
CONS:
- The long running task is not truly cancelled, it still executes in the background. Control is just given back to the main processing thread
Task.WaitAsync
The new Task.WaitASync
method performs the same function as the above technique, it is just much cleaner.
try
{
// start the long running process, and wait on the task
// for a maximum of 1 second
await LongRunningProcessAsync()
.WaitAsync(TimeSpan.FromSeconds(1));
}
catch (TimeoutException)
{
Console.WriteLine($"{DateTime.Now.TimeOfDay} => " +
$"Download took too long and was cancelled");
}
}
// This method is unable to change, so no CancellationToken can be used
async Task LongRunningProcessAsync()
{
Console.WriteLine($"{DateTime.Now.TimeOfDay} => Starting large download...");
for (int x = 0; x < 4; x++)
{
await Task.Delay(1000);
}
Console.WriteLine($"{DateTime.Now.TimeOfDay} => Download Complete");
}
The output is as follows:
18:02:46.4930592 => Starting large download...
18:02:47.5520137 => Download took too long and was cancelled
18:02:50.5424670 => Download Complete
As can be seen, the resulting output is the same, however the code required is a lot more compact and cleaner. Unfortunately, even with this new method, without a cancellation token, the long running task will still execute in the background.
PROS:
- A long running task can be "cancelled" even without a cancellation token
CONS:
- The long running task is not truly cancelled, it still executes in thee background. Control is just given back to the main processing thread
Conclusion
The preferred solution for cancelling a long running task is to use a CancellationToken., and if this is available should always be used. However this is not always possible, and in that case the Task.WaitAsync
is the best and cleanest way of handling a task "cancellation".
References
New Task.WaitAsync method in .NET 6
Daily Drop 12: 16-02-2022
At the start of 2022 I set myself the goal of learning one new coding related piece of knowledge a day.
It could be anything - some.NET / C# functionality I wasn't aware of, a design practice, a cool new coding technique, or just something I find interesting. It could be something I knew at one point but had forgotten, or something completely new, which I may or may never actually use.
The Daily Drop is a record of these pieces of knowledge - writing about and summarizing them helps re-enforce the information for myself, as well as potentially helps others learn something new as well.