How to cancel remaining tasks
The magic lies in the CancellationTokenSource. The idea is to create a token which we pass to every asynchronous action. Once a task is completed we abort every other task. Let's do it:
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;
// Abort after 1 second
// If any of these tasks need longer than this time period you'll get an
// System.Threading.Tasks.TaskCanceledException and all tasks are cancelled
cts.CancelAfter(1000);
Task<string> taskWinner = await Task.WhenAny(new[]
{ GetStringAsync("someurl", ct), GetStringAsync("anotherurl", ct) });
// Cancel all remaining tasks
cts.Cancel();
// As long as one Task finishes without getting cancelled or without
// an exception we have always a result without an error
string content = await taskWinner;
// Pass the token down the chain
private async Task GetStringAsync(string url, CancellationToken token)
{
using var new httpClient = new HttpClient();
// Token gets passed to
return await httpClient.GetStringAsync(url, token);
}
The first line creates the CancellationTokenSource
. From that object we can get the token itself. We also say that the operations in question are not allowed to run longer than 1000 milliseconds / 1 second.
await Task.WhenAny(new[]
{ GetStringAsync("someurl", ct), GetStringAsync("anotherurl", ct) });
We are passing the same token to every asynchronous task and await Task.WhenAny
. Once the first tasks finishes Task.WhenAny
will return and we immediately cancel the CancellationTokenSource
thus every tasks receive the "cancel" signal.
This can save precious resources.
Special cases
The first finished task is in a faulted state
Let's have a look at this quick test:
[Fact]
public async Task ShouldThrow()
{
var faultTask = Task.FromException(new Exception("Test"));
var normalTask = Task.Delay(100);
Func<Task> any = async () => await Task.WhenAny(faultTask, normalTask);
await any.Should().NotThrowAsync();
}
This test will pass. Even though the first tasks throws an exception, the second one finishes without any problem. Therefore Task.WhenAny
will return the successful ran task.
Timeout
Now back to our original example. Let's assume none of our tasks will make it in time. What will happen? Or general speaking what will happen if all tasks are in faulty state?
[Fact]
public async Task ShouldThrow()
{
var source = new CancellationTokenSource();
source.CancelAfter(50);
var token = source.Token;
var timeoutTask = Task.Delay(500, token);
Func<Task> any = async () => await Task.WhenAny(new[] { timeoutTask });
await any.Should().ThrowAsync<TaskCanceledException>();
}
This test will not pass. Task.WhenAny
will not throw an exception! Even if all your tasks are throwing exceptions, Task.WhenAny
will always return a Task
-object with state RanToCompletion
. Let's change the test in that manner, that we await the returning task of Task.WhenAny
:
public async Task ShouldNotThrow()
{
var source = new CancellationTokenSource();
source.CancelAfter(50);
var token = source.Token;
var timeoutTask = Task.Delay(500, token);
var taskFromWhenAny = await Task.WhenAny(new[] { timeoutTask });
Func<Task> any = async () => await taskFromWhenAny;
await any.Should().ThrowAsync<TaskCanceledException>();
}
Now we are awaing the returned task and boom, we get a passing test. That means the awaited result of Task.WhenAny
throws an exception.
Conclusion
Task.WhenAny
can be a good canidate if you want to get a resource from multiple sources and abort all other tasks once the first finishes. Be aware that Task.WhenAny
doesn't throw any TaskCancelledException
so that you have to check the state of the task afterwards.