Search code examples
c#async-awaittaskdotnet-httpclientcancellation-token

How to cancel other threads as soon as one completed thread satisfies condition


I have an ASP.NET MVC application which needs to check if something exists at 3 remote API servers. The application passes an ID to each API and it returns either true or false. The code looks like this.

public class PingController
{
    public async Task<bool> IsFound(int id)
    {
        var servers = new ['a.com', b.com', 'c.com'];
        var result = await foundAtServers(id, servers);
        return result;
    }

    private async Task<bool> foundAtServers(int id, string[] servers)
    {
        var tasks = from server in servers
                    select checkServer(id, server);

        return await.Task.WhenAll(tasks.ToArray());
    }

    private async Task<bool> checkServer(id, server)
    {
         var request = new HttpRequestMessage(HttpMethod.Get, server+"/api/exists"+id);
         var client = new HttpClient();

         var task = await client.SendAsync(request);
         var response = await task.Content.ReadAsStringAsync();

         return bool.Parse(response);
    }
}

This code currently checks all 3 APIs asynchronously but will wait until ALL of the HttpClient calls have completed before the MVC Action can return.

As soon as one API returns true I want to immediately return true on the Action, rather than wait for the other tasks to complete.

The C# Task class has .WaitAll and .WaitAny, but these won't work either. As I need to cancel the other HttpClient request, I presume I need to use a CancellationToken but I don't know how to use it with this structure.

Cheers.


Solution

  • If you want to immediately return, you can use Task.WhenAny instead of Task.WhenAll. This won't cancel the on-going tasks, but it will enable you to return as soon as possible:

    private async Task<bool> FoundAtServersAsync(int id, string[] servers)
    {
        var tasks = (from server in servers
                     select checkServer(id, server)).ToList();
    
        while (tasks.Count > 0)
        {
            var finishedTask = await Task.WhenAny(tasks);
            if (finishedTask.Result)
            {
                return finishedTask.Result;
            }
    
            tasks.Remove(finishedTask);
        }
        return false;
    }
    

    This will discard the other tasks. This means that if any exception is thrown inside one of them, it will be swallowed.

    Edit:

    If you care about actually canceling the other tasks, consider passing your CancellationToken to the overload of SendAsync which takes one, and calling CancellationTokenSource.Cancel once a value is received. Note this will mean you'll also need to handle the OperationCanceledException they will throw.

    If they don't matter, i'd simply discard them as above.