Search code examples
c#asynchronousasync-awaittask-parallel-library

How to implement Task.WhenAny() with a predicate


I want to execute several asynchronous tasks concurrently. Each task will run an HTTP request that can either complete successfully or throw an exception. I need to await until the first task completes successfully, or until all the tasks have failed.

How can I implement an overload of the Task.WhenAny method that accepts a predicate, so that I can exclude the non-successfully completed tasks?


Solution

  • public static Task<T> GetFirstResult<T>(
        ICollection<Func<CancellationToken, Task<T>>> taskFactories, 
        Predicate<T> predicate) where T : class
    {
        var tcs = new TaskCompletionSource<T>();
        var cts = new CancellationTokenSource();
    
        int completedCount = 0;
        // in case you have a lot of tasks you might need to throttle them 
        //(e.g. so you don't try to send 99999999 requests at the same time)
        // see: http://stackoverflow.com/a/25877042/67824
        foreach (var taskFactory in taskFactories)
        {
            taskFactory(cts.Token).ContinueWith(t => 
            {
                if (t.Exception != null)
                {
                    Console.WriteLine($"Task completed with exception: {t.Exception}");
                }
                else if (predicate(t.Result))
                {
                    cts.Cancel();
                    tcs.TrySetResult(t.Result);
                }
    
                if (Interlocked.Increment(ref completedCount) == taskFactories.Count)
                {
                    tcs.SetException(new InvalidOperationException("All tasks failed"));
                }
    
            }, cts.Token);
        }
    
        return tcs.Task;
    }
    

    Sample usage:

    using System.Net.Http;
    var client = new HttpClient();
    var response = await GetFirstResult(
        new Func<CancellationToken, Task<HttpResponseMessage>>[] 
        {
            ct => client.GetAsync("http://microsoft123456.com", ct),
            ct => client.GetAsync("http://microsoft123456.com", ct),
            ct => client.GetAsync("http://microsoft123456.com", ct),
            ct => client.GetAsync("http://microsoft123456.com", ct),
            ct => client.GetAsync("http://microsoft123456.com", ct),
            ct => client.GetAsync("http://microsoft123456.com", ct),
            ct => client.GetAsync("http://microsoft123456.com", ct),
            ct => client.GetAsync("http://microsoft.com", ct),
            ct => client.GetAsync("http://microsoft123456.com", ct),
            ct => client.GetAsync("http://microsoft123456.com", ct),
        }, 
        rm => rm.IsSuccessStatusCode);
    Console.WriteLine($"Successful response: {response}");