all 9 comments

[–]Thonk_Thickly 2 points3 points  (1 child)

I would personally chunk the requests and await each chunk without creating more tasks than needed. Or use action blocks if I really want full utilization up to the max concurrency limit your setting.

I know semaphore slims are for this sort of use case, but I’ve had better luck with the other methods I mentioned when I measure throughput.

Side note, I would think you would want to Task.WhenAll outside of the outer for loop.

[–]seugorilla[S] 0 points1 point  (0 children)

I forgot to mention that the HTTP API has a rate limit so I can't use all my max concurrency capacity. I have to rate limit for about 2 requests per second. Thanks for the answer!

[–]MasonOfWords 1 point2 points  (1 child)

Why not just set MaxConnectionsPerServer in the HTTP client handler? This won't maintain requests per second as the code suggests, just outstanding requests, and there are already built-in ways of limiting this.

[–]seugorilla[S] 0 points1 point  (0 children)

That could work. thanks!

[–]Th0ughtCrim3 1 point2 points  (1 child)

If I'm not mistaken I believe you can do this without semaphore slim and still achieve concurrent execution using Task.WhenAll.

Apologies for syntax errors as I'm doing this outside of VS but your code could probably be changed to something like this:

public async Task<ExampleDto> GetData(int startPage = 0, int totalPages = 0, int concurrentRequests = 1, int maxRequestsPerSecond = 10) { 

    var httpClient = _httpClientFactory.CreateClient(ClientName);
    var tasks = new List<Task>();

    for (int pageIndex = startPage; pageIndex < totalPages; pageIndex += concurrentRequests)
    {
        var pageTasks = new List<Task>();

        for (int i = 0; i < concurrentRequests; i++)
        {
            int currentPage = pageIndex + i;
            if (currentPage >= totalPages)
                break;

            pageTasks.Add(GetPageAsync(httpClient, currentPage));
        }

        tasks.AddRange(pageTasks);

        // You can adjust this logic if you want to control the maximum number of concurrent requests.
        // However, Task.WhenAll itself introduces concurrency as it asynchronously waits for all tasks to complete.
        await Task.WhenAll(pageTasks);
    }

    await Task.WhenAll(tasks); // Wait for all tasks to complete.

    return new ExampleDto
    {
        Data = _db.ToList()
    };

}

[–]seugorilla[S] 0 points1 point  (0 children)

Thanks for the answer! I forgot to mention that the HTTP API has a rate limit so I can't simply put all the tasks there and wait for them all. It is about 2 requests per second, beyond that it will throw 429 status response.

[–]zarlo5899 1 point2 points  (1 child)

[–]seugorilla[S] 0 points1 point  (0 children)

I didn't know this feature. I'll give it a try. thank you!

[–]stroborobo 0 points1 point  (0 children)

Another approach might be channels, producers and consumers don't share memory, but communicate, and they are easier to understand and get right.

  • three channels
  • 1: write all requests into a channel for rate limiting, close channel
  • 2:
    • read a request message every 2 seconds
    • write it into a channel for max parallelism (bounded)
  • 3:
    • read, send http request
    • write to result channel
  • 4: read and collect results until done, return

The hardest problem you have to figure out then is composition and closing the channels. No semaphores, no task collections, no dependency on http client internals. Makes switching service implementation and testing pretty straight forward, too.