2

I'm building a solution to find a desired value from an API call inside a for loop.

I basically need to pass to an API the index of a for loop, one of those index will return a desired output, in that moment I want the for loop to break, but I need to efficient this process. I thought making it asynchronous without the await, so that when some API returns the desired output it breaks the loop.

Each API call takes around 10sec, so if I make this async or multithread I would reduce the execution time considerably.

I haven't fount any good orientation of how making async / not await HTTP request.

Any suggestions?

for (int i = 0; i < 60000; i += 256)
{            
    Console.WriteLine("Incrementing_value: " + i);
    string response = await client.GetStringAsync(
        "http://localhost:7075/api/Function1?index=" + i.ToString());
    Console.WriteLine(response);                
    if (response != "null")//
    {
        //found the desired output
        break;
    }
}
  • Do you want to launch the asynchronous operations concurrently, but impose a [limit to the concurrency of these operations](https://stackoverflow.com/questions/10806951/how-to-limit-the-amount-of-concurrent-async-i-o-operations)? – Theodor Zoulias Jun 18 '21 at 01:39
  • IMHO write an async method for the loop body, use a SemaphoreSlim to limit concurrency, create a CancellationTokenSource and cancel it when you find what you're looking for? (edit, that looks almost like the answer that just appeared.... ) – Jeremy Lakeman Jun 18 '21 at 01:48

2 Answers2

1

You can run requests in parallel, and cancel them once you have found your desired output:

public class Program {

    public static async Task Main() {
        var cts = new CancellationTokenSource();
        var client = new HttpClient();

        var tasks = new List<Task>();

        // This is the number of requests that you want to run in parallel.
        const int batchSize = 10;

        int requestId = 0;
        int batchRequestCount = 0;

        while (requestId < 60000) {
            if (batchRequestCount == batchSize) {
                // Batch size reached, wait for current requests to finish.
                await Task.WhenAll(tasks);
                tasks.Clear();
                batchRequestCount = 0;
            }

            tasks.Add(MakeRequestAsync(client, requestId, cts));
            requestId += 256;
            batchRequestCount++;
        }

        if (tasks.Count > 0) {
            // Await any remaining tasks
            await Task.WhenAll(tasks);
        }
    }

    private static async Task MakeRequestAsync(HttpClient client, int index, CancellationTokenSource cts) {
        if (cts.IsCancellationRequested) {
            // The desired output was already found, no need for any more requests.
            return;
        }

        string response;

        try {
            response = await client.GetStringAsync(
                "http://localhost:7075/api/Function1?index=" + index.ToString(), cts.Token);
        }
        catch (TaskCanceledException) {
            // Operation was cancelled.
            return;
        }

        if (response != "null") {
            // Cancel all current connections
            cts.Cancel();

            // Do something with the output ...
        }
    }

}

Note that this solution uses a simple mechanism to limit the amount of concurrent requests, a more advanced solution would make use of semaphores (as mentioned in some of the comments).

JosephDaSilva
  • 1,107
  • 4
  • 5
  • Found and error on response = await client.GetStringAsync( "localhost:7075/api/Function1?index=" + index.ToString(), cts.Token); -> ERROR -> (awaitable) TASKHttpClient.GetStringAsync(string requestUri) (+1 overload) Send Get request to the specified Uri and return the response body as string in an asynchronous operation. Returns: the task object representing the asynchronous operation. Exceptions: ArgumentNullException HttpRequestException CS1501: No overload for method 'GetStringAsync' takes 2 arguments. – Christopher Martinez Jun 18 '21 at 02:29
  • 1
    GetStringAsync with CancellationToken is only available in .NET 5+. If you are using an earlier version, you can use [`HttpClient.GetAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getasync?view=netcore-3.1). – JosephDaSilva Jun 18 '21 at 02:36
1

There are multiple ways to solve this problem. My personal favorite is to use an ActionBlock<T> from the TPL Dataflow library as a processing engine. This component invokes a provided Action<T> delegate for every data element received, and can also be provided with an asynchronous delegate (Func<T, Task>). It has many useful features, including (among others) configurable degree of parallelism/concurrency, and cancellation via a CancellationToken. Here is an implementation that takes advantage of those features:

async Task<string> GetStuffAsync()
{
    var client = new HttpClient();
    var cts = new CancellationTokenSource();
    string output = null;

    // Define the dataflow block
    var block = new ActionBlock<string>(async url =>
    {
        string response = await client.GetStringAsync(url, cts.Token);
        Console.WriteLine($"{url} => {response}");
        if (response != "null")
        {
            // Found the desired output
            output = response;
            cts.Cancel();
        }
    }, new ExecutionDataflowBlockOptions()
    {
        CancellationToken = cts.Token,
        MaxDegreeOfParallelism = 10 // Configure this to a desirable value
    });

    // Feed the block with URLs
    for (int i = 0; i < 60000; i += 256)
    {
        block.Post("http://localhost:7075/api/Function1?index=" + i.ToString());
    }
    block.Complete();

    // Wait for the completion of the block
    try { await block.Completion; }
    catch (OperationCanceledException) { } // Ignore cancellation errors
    return output;
}

The TPL Dataflow library is built-in the .NET Core / .NET 5. and it is available as a package for .NET Framework.

The upcoming .NET 6 will feature a new API Parallel.ForEachAsync, that could also be used to solve this problem in a similar fashion.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104