Calling async method from a loop

alexteslin

Member
Joined
Oct 28, 2022
Messages
10
Programming Experience
3-5
Hi,

I have a loop from which I call an async method that in turn calls several async methods. My loop example:
Loop:
List<Task<ReturnItem>> savedItemTasks = new List<Task<ReturnItem>>();
foreach(var item in items)
{
    savedItemTasks.Add(SaveItem(item))
}

var result = await Task.WhenAll<ReturnItem>(savedItemTasks);

My SaveItem method example:

SaveItem:
private async Task<ReturnItem> SaveItem(ItemType item)
{
    if(!IsValidItem(item))
        return;
    
    Guid? itemId = await GetItemId(item);
    
    if(itemId == null || itemId == Guid.Empty)
        return;
    
    var sourceId = await GetSourceId(item);
    
    if(sourceId == null || sourceId == Guid.Empty)
        sourceId = await CreateSourceId(item);
    
    // Some other async calls
    
    return new ReturnItem {
        ...
    };
}

The above loop works fine and in async. The problem is within the SaveItem() method when first checks for existing sourceId by calling GetSourceId() method and if it can't find then calls CreateSourceId() method. And the problem is that the CreateSourceId method depends on previous GetSourceId() method. Even though I am using await keywords these are called from the loop. And if the sourceId is null or empty guid on first iteration it creates correctly the sourceId by calling CreateSourceId() method. But if the second immediate item has no sourceId either it then calls again for CreateSourceId(), in which case it creates a duplicate.
What I am after is that to wait until the sourceId from the previous item has been created so that then I can use that value.
Is this possible at all? I don't want to make the loop synchronous as there are quite a number of items and there are more methods and saving to db etc, which will take much longer to run the code.

Thanks,
Alex

My
 
Solution
Not within the loop. The loop is irrelevant here as I noted above. If you unroll the loop you still have the same issue. The issue is the two consecutive calls and your current architecture being unable to handle it.

You'll need some kind of synchronization around the get-and-create-if-not-present logic.
As I recall, the synchronization context for ASP.NET will use the current thread pool thread that is handling the request. It won't schedule any continuations on another thread, so there shouldn't be any other threads in play with the code in post #1.
 
My recollection above is for .NET Framework ASP.NET. If you are using ASP.NET Core, then there is no synchronization context -- just like with a console app -- and there you'll get multiple threads.

 
Yes, there is a problem. If you run only for few items it seemed working, but when I run for hundreeds then the code breaks. I will try with async those methods and if still having issues then the only solution I am thinking of creating a key-value collection with Get and Create and only then call the loop. So that SaveItem() method does not need to check and create SourceIds.
 
Using the semphore as a lock like you did above should work.

There's also an alternative way using a (concurrent) dictionary to return a Task<ID> which will be sort of equivalent to what you are thinking with a key-value collection.
 
Why don't you dedupe before you loop? Or, if different things are done with each duplicate, restructure things so items are grouped under their duplicate aspect and then processed in a loop-in-loop (outer loop loops groups, inner loop processes loop items by awaiting the first get or create then can WhenAll a skip(1))

You should suffix your Async methods with "...Async" by the way
 
Currently it works as expected and all the data stored correctly to db. Although, not sure how it will affect the performance and hence was thinking on Dictionary. But the way I was thinking to use the dictionary was to store all the items in there first before calling the main loop in sync way. There will be no other calls to this method/s. I am not sure whether using concurrent will improve the performance, but if I go to the dictionary root it will be worth trying it. Thanks
 
I was thinking of using the ConcurrentDictionary to hold on to tasks which are trying to lookup and if needed create the id. See lines 61-74. Obviously, that dictionary fill up, but I'll leave that as an exercise for how to implement an LRU on that dictionary so that ids that are not recently used in the last few minutes are evicted out.

With SaveItemOne() notice that the output, shows that different IDs are generated for the same name, but SaveItemTwo() does not. This is because only one task ends up doing the work for searching and creating, and once an id has been found or created, it stays with the task.


Here's some sample code written for .NET 6 Console.
C#:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

await DoTest(SaveItemOne);
await DoTest(SaveItemTwo);

async Task<Guid> SaveItemOne(SourceIdRepository repo, string name)
{
    var id = await repo.GetSourceIdAsync(name);
    if (id == Guid.Empty)
        id = await repo.CreateSourceIdAsync(name);
    return id;
}

async Task<Guid> SaveItemTwo(SourceIdRepository repo, string name)
{
    return await repo.GetOrCreateSourceIdAsync(name);
}

async Task DoTest(Func<SourceIdRepository, string, Task<Guid>> saveItem)
{
    Console.WriteLine("*** Doing Test ***");

    var names = Enumerable.Repeat("unknown", 5).ToList();
    names.Add("WellKnown1");
    names.Add("WellKnown2");
    names.Add("WellKnown1");
    names.Add("other");
    var tasks = new List<Task<Guid>>();
    var repo = new SourceIdRepository();
    foreach (var name in names)
        tasks.Add(saveItem(repo, name));

    var results = await Task.WhenAll(tasks);
    foreach (var result in names.Zip(results, (n, r) => (n, r)))
        Console.WriteLine($"{result.n}: {result.r}");

    Console.WriteLine();
}


class SourceIdRepository
{
    readonly MockDatabase _db = new ();

    public async Task<Guid> GetSourceIdAsync(string name)
    {
        var id = await _db.GetAsync(name);
        return id;
    }

    public async Task<Guid> CreateSourceIdAsync(string name)
    {
        var id = await _db.CreateAsync(name);
        return id;
    }

    readonly ConcurrentDictionary<string, Task<Guid>> _idsToNames = new();

    public async Task<Guid> GetOrCreateSourceIdAsync(string name)
    {
        async Task<Guid> GetOrCreateReally(string name)
        {
            var id = await _db.GetAsync(name);
            if (id == Guid.Empty)
                id = await _db.CreateAsync(name);
            return id;
        }

        return await _idsToNames.GetOrAdd(name, GetOrCreateReally);
    }
}

class MockDatabase
{
    const int MinDelayMilliseconds = 500;
    const int MaxDelayMilliseconds = 5000;

    readonly Random _random = new(123);
    readonly Random _idsRandom = new(789);

    async Task SimulateWork()
    {
        await Task.Delay(_random.Next(MinDelayMilliseconds, MaxDelayMilliseconds));
    }

    Dictionary<string, Guid> _nameToIds = new()
    {
        ["WellKnown1"] = new Guid("3EB0C238-4347-4770-BAAB-403307015826"),
        ["WellKnown2"] = new Guid("09FA95A5-2D37-41EA-88EF-A84260804D5E"),
    };

    public async Task<Guid> GetAsync(string name)
    {
        Console.WriteLine($"{nameof(GetAsync)}({name})");
        await SimulateWork();
        lock (_nameToIds)
        {
            var id = _nameToIds.ContainsKey(name) ? _nameToIds[name] : Guid.Empty;
            Console.WriteLine($"Get {name}: {id}");
            return id;
        }
    }

    Guid CreateGuid()
    {
        var bytes = new byte[16];
        _idsRandom.NextBytes(bytes);
        return new Guid(bytes);
    }

    public async Task<Guid> CreateAsync(string name)
    {
        Console.WriteLine($"{nameof(CreateAsync)}({name})");
        await SimulateWork();
        lock (_nameToIds)
        {
            var id = CreateGuid();
            var op = "";
            if (_nameToIds.ContainsKey(name))
            {
                op = "Overwriting";
                _nameToIds[name] = id;
            }
            else
            {
                op = "Creating";
                _nameToIds.Add(name, id);
            }
            Console.WriteLine($"{op} {name}: {id}");
            return id;
        }
    }
}

The output I get on a test run:
C#:
*** Doing Test ***
GetAsync(unknown)
GetAsync(unknown)
GetAsync(unknown)
GetAsync(unknown)
GetAsync(unknown)
GetAsync(WellKnown1)
GetAsync(WellKnown2)
GetAsync(WellKnown1)
GetAsync(other)
Get WellKnown2: 09fa95a5-2d37-41ea-88ef-a84260804d5e
Get WellKnown1: 3eb0c238-4347-4770-baab-403307015826
Get WellKnown1: 3eb0c238-4347-4770-baab-403307015826
Get other: 00000000-0000-0000-0000-000000000000
CreateAsync(other)
Get unknown: 00000000-0000-0000-0000-000000000000
CreateAsync(unknown)
Get unknown: 00000000-0000-0000-0000-000000000000
CreateAsync(unknown)
Get unknown: 00000000-0000-0000-0000-000000000000
CreateAsync(unknown)
Get unknown: 00000000-0000-0000-0000-000000000000
CreateAsync(unknown)
Creating other: a699f33d-dc60-0b31-0212-376d83726760
Get unknown: 00000000-0000-0000-0000-000000000000
CreateAsync(unknown)
Creating unknown: d956f610-c4ac-7273-f962-4be6a9014dec
Overwriting unknown: e1b59e41-a93d-29bd-9f2d-50eb6e297f68
Overwriting unknown: 27cce7d9-9ad8-793b-7f26-adfd904a6101
Overwriting unknown: e42b4bc4-85bd-6723-382c-3a7e738703cf
Overwriting unknown: c10de2e3-2635-a714-633b-6116fbc09954
unknown: e1b59e41-a93d-29bd-9f2d-50eb6e297f68
unknown: e42b4bc4-85bd-6723-382c-3a7e738703cf
unknown: 27cce7d9-9ad8-793b-7f26-adfd904a6101
unknown: d956f610-c4ac-7273-f962-4be6a9014dec
unknown: c10de2e3-2635-a714-633b-6116fbc09954
WellKnown1: 3eb0c238-4347-4770-baab-403307015826
WellKnown2: 09fa95a5-2d37-41ea-88ef-a84260804d5e
WellKnown1: 3eb0c238-4347-4770-baab-403307015826
other: a699f33d-dc60-0b31-0212-376d83726760

*** Doing Test ***
GetAsync(unknown)
GetAsync(WellKnown1)
GetAsync(WellKnown2)
GetAsync(other)
Get WellKnown2: 09fa95a5-2d37-41ea-88ef-a84260804d5e
Get other: 00000000-0000-0000-0000-000000000000
CreateAsync(other)
Get WellKnown1: 3eb0c238-4347-4770-baab-403307015826
Get unknown: 00000000-0000-0000-0000-000000000000
CreateAsync(unknown)
Creating unknown: a699f33d-dc60-0b31-0212-376d83726760
Creating other: d956f610-c4ac-7273-f962-4be6a9014dec
unknown: a699f33d-dc60-0b31-0212-376d83726760
unknown: a699f33d-dc60-0b31-0212-376d83726760
unknown: a699f33d-dc60-0b31-0212-376d83726760
unknown: a699f33d-dc60-0b31-0212-376d83726760
unknown: a699f33d-dc60-0b31-0212-376d83726760
WellKnown1: 3eb0c238-4347-4770-baab-403307015826
WellKnown2: 09fa95a5-2d37-41ea-88ef-a84260804d5e
WellKnown1: 3eb0c238-4347-4770-baab-403307015826
other: d956f610-c4ac-7273-f962-4be6a9014dec
 
Last edited:
Back
Top Bottom