2

The case is that I call an API once to get a list of tenants, then for each tenant I must call again to get a list of usages for the tenant. Unfortunately there is no way to get usages for all tenants in a single call.

Now I wish to try to save time by making these calls concurrent. Then put them all together after the last one arrives. Here is what my attempt looks like so far:

public async Task<List<Usage>> GetUsagesAsync2(Task<List<Tenant>> tenants)
{
    List<Usage> usages = new List<Usage>();

    foreach (var tenant in await tenants)
    {
        //Generate request
        RestRequest request = new RestRequest("tenants/{tenantID}/usages", Method.Get);
        request.AddParameter("tenantID", tenant.id, ParameterType.UrlSegment);
        request.AddHeader("Authorization", $"Bearer {Token.access_token}");

        //Get response
        RestResponse response = await _client.ExecuteAsync(request)
            .ConfigureAwait(false);

        //Validate response
        if (response.StatusCode != HttpStatusCode.OK)
            throw new Exception("Failed at Getting usages for a tenant: ");

        //Add to list
        var t_usage = JsonConvert.DeserializeObject<Wrapper<Usage>>(response.Content);
        usages.AddRange(t_usage.items);
    }
    return usages;
}

The code runs and does what it is supposed to, but I am not sure that it actually runs asynchronously. Takes about 7-8 seconds to run, which I find a bit long to wait on a webpage.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 7
    Async != parallel != concurrent – nbokmans Aug 08 '22 at 11:25
  • 1
    To perform multiple async tasks at the same time, make use of the `await Task.WhenAll(Tasks[])` method – nbokmans Aug 08 '22 at 11:27
  • 3
    Just be aware that when using`WhenAll`, ALL the tasks will be running at the same time, with no throttling. This could mean hundreds or even thousands of concurrent API calls. You should use some kind of producer / consumer queue, or other throttling mechanism to keep things under control. – Bradley Uffner Aug 08 '22 at 11:47
  • 1
    @TheodorZoulias I will do that. Yeah it is probably concurrent I am looking for. I'm not at all certain what the difference is between the 3 as nbokmans guessed about me. – hans nordblad Aug 08 '22 at 12:32
  • This might be helpful: [What is the difference between concurrency, parallelism and asynchronous methods?](https://stackoverflow.com/questions/4844637/what-is-the-difference-between-concurrency-parallelism-and-asynchronous-methods) – Theodor Zoulias Aug 08 '22 at 12:46
  • 1
    @TheodorZoulias .net framework 4.6.2 – hans nordblad Aug 08 '22 at 12:49

2 Answers2

2

Here is parallelized implementation:

.NET 6

public async Task<ConcurrentBag<Usage>> GetUsagesAsync2(Task<List<Tenant>> tenants)
{
    ConcurrentBag<Usage> usages = new ConcurrentBag<Usage>();

    await Parallel.ForEachAsync(await tenants, async (tenant) =>
    {
        //Generate request
        RestRequest request = new RestRequest("tenants/{tenantID}/usages", Method.Get);
        request.AddParameter("tenantID", tenant.id, ParameterType.UrlSegment);
        request.AddHeader("Authorization", $"Bearer {Token.access_token}");

        //Get response
        RestResponse response = await _client.ExecuteAsync(request).ConfigureAwait(false);

        //Validate response
        if (response.StatusCode != HttpStatusCode.OK)
            throw new Exception("Failed at Getting usages for a tenant: ");

        //Add to list
        var t_usage = JsonConvert.DeserializeObject<Wrapper<Usage>>(response.Content);
        usages.AddRange(t_usage.items);
    });

    return usages;
}

Pre .Net 6

public async Task<ConcurrentBag<Usage>> GetUsagesAsync2(Task<List<Tenant>> tenants)
{
    ConcurrentBag<Usage> usages = new ConcurrentBag<Usage>();
    var tasks = new List<Task>();

    foreach (var tenant in await tenants)
    {
        tasks.Add(Task.Run(async () =>
        {
            //Generate request
            RestRequest request = new RestRequest("tenants/{tenantID}/usages", Method.Get);
            request.AddParameter("tenantID", tenant.id, ParameterType.UrlSegment);
            request.AddHeader("Authorization", $"Bearer {Token.access_token}");

            //Get response
            RestResponse response = await _client.ExecuteAsync(request)
                .ConfigureAwait(false);

            //Validate response
            if (response.StatusCode != HttpStatusCode.OK)
                throw new Exception("Failed at Getting usages for a tenant: ");

            //Add to list
            var t_usage = JsonConvert.DeserializeObject<Wrapper<Usage>>(response.Content);
            usages.AddRange(t_usage.items);
        }));
    }

    await Task.WhenAll(tasks);

    return usages;
}

Take note that List is changed to ConcurrentBag because it is not thread safe

Iavor Orlyov
  • 512
  • 1
  • 4
  • 15
  • I had to change method return type to Task and changed usages.AddRange to t_usage.items.ForEach(i=> usages.Add(i)) to get the code to run. When I look in the bag it is empty however... :( – hans nordblad Aug 08 '22 at 11:46
  • though if I look in the bag during the foreach then I see it get filled up. Why is it empty outside the method? – hans nordblad Aug 08 '22 at 12:00
  • As it seems Parallel does not work very well with async Action. If you are using .net 6 you can use await Parallel.ForEachAsync – Iavor Orlyov Aug 08 '22 at 12:03
  • gah! I´m stuck at .net framework 4.6 – hans nordblad Aug 08 '22 at 12:15
  • Updated my answer. Could you please try with second solution – Iavor Orlyov Aug 08 '22 at 12:23
  • The `ConcurrentBag` is not a suitable collection for collecting the results of a parallel loop, mainly because it doesn't preserve the order of the inserted items. For more info and alternatives you can look [here](https://stackoverflow.com/questions/15400133/when-to-use-blockingcollection-and-when-concurrentbag-instead-of-listt/64823123#64823123 "When to use BlockingCollection and when ConcurrentBag instead of List?"). – Theodor Zoulias Aug 08 '22 at 12:27
  • I agree with you but I can't see where the author wanted the same order anyway – Iavor Orlyov Aug 08 '22 at 12:28
  • There is no need to collect the async results yourself anyway. `Task.WhenAll()` has an overload that does it for you. See [my answer](https://stackoverflow.com/a/73277945/1025555). Flattening of the results can be easily done with `SelectMany()` afterwards. – Good Night Nerd Pride Aug 08 '22 at 12:36
  • I don't care about the order by the way. tenants and usages are easy enough to put back together. – hans nordblad Aug 08 '22 at 12:40
  • The OP has said in the question that *"The code runs and does what it is supposed to."* By changing the order of the `Usage` in the `List`, the code might no longer do what it is supposed to do. IMHO the default assumption should be that the OP wants to preserve the current behavior, even if they haven't expressed this desire explicitly in the question. – Theodor Zoulias Aug 08 '22 at 12:41
  • 1
    It worked. Will accept as answer. Brought the runtime down to 1.1 second. A good improvement over the previous 7 seconds. I probably still need to learn more about parallel and concurrent but this will do for now. – hans nordblad Aug 08 '22 at 12:43
1

What you could basically do is:

IEnumerable<Task<List<Usage>> loadTasks = tenants.Select(LoadUsages);
List<Usage>[] usages = await Task.WhenAll(loadTasks);

async Task<List<Usage>> LoadUsages(Tenant t) {
    // your web call goes here
    return t_usage.items;
}

But, as pointed out in the comments, this will not be throttled and might issue way too many requests at once. If you're sure the number of tenants will stay around 20 this should be fine. Otherwise you'll have to implement a more sophisticated solution that does batch processing.

Good Night Nerd Pride
  • 8,245
  • 4
  • 49
  • 65
  • 1
    Here is a question related to throttling: [How to limit the amount of concurrent async I/O operations?](https://stackoverflow.com/questions/10806951/how-to-limit-the-amount-of-concurrent-async-i-o-operations) – Theodor Zoulias Aug 08 '22 at 12:54
  • @TheodorZoulias Thanks. For now the amount of tenants is 24. If it grows much further I will implement throttling. – hans nordblad Aug 08 '22 at 13:09
  • 1
    @TheodorZoulias So, there I implemented the SemaphoreSlim mentioned in the answer you linked to. Seems to work fine and only adds ~0.5 seconds. – hans nordblad Aug 08 '22 at 13:20