In my app I am creating some concurrent web requests and I am satisfied when any one of them completes, so I am using the method Task.WhenAny
:
var urls = new string[] {
"https://stackoverflow.com",
"https://superuser.com",
"https://www.reddit.com/r/chess",
};
var tasks = urls.Select(async url =>
{
using (var webClient = new WebClient())
{
return (Url: url, Data: await webClient.DownloadStringTaskAsync(url));
}
}).ToArray();
var firstTask = await Task.WhenAny(tasks);
Console.WriteLine($"First Completed Url: {firstTask.Result.Url}");
Console.WriteLine($"Data: {firstTask.Result.Data.Length:#,0} chars");
First Completed Url: https://superuser.com
Data: 121.954 chars
What I don't like to this implementation is that the non-completed tasks continue downloading data I no longer need, and waste bandwidth I would prefer to preserve for my next batch of requests. So I am thinking about cancelling the other tasks, but I am not sure how to do it. I found how to use a CancellationToken
to cancel a specific web request:
public static async Task<(string Url, string Data)> DownloadUrl(
string url, CancellationToken cancellationToken)
{
try
{
using (var webClient = new WebClient())
{
cancellationToken.Register(webClient.CancelAsync);
return (url, await webClient.DownloadStringTaskAsync(url));
}
}
catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled)
{
cancellationToken.ThrowIfCancellationRequested();
throw;
}
}
Now I need an implementation of Task.WhenAny
that will take an array of urls, and will use my DownloadUrl
function to fetch the data of the fastest responding site, and will handle the cancellation logic of the slower tasks. It would be nice if it had a timeout argument, to offer protection against never-ending tasks. So I need something like this:
public static Task<Task<TResult>> WhenAnyEx<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, CancellationToken, Task<TResult>> taskFactory,
int timeout)
{
// What to do here?
}
Any ideas?