Search code examples
c#async-awaitconcurrencytask-parallel-libraryconcurrent-collections

Collecting results of async within lambda


I have these example code:

    private async Task<IEnumerable<long>> GetValidIds1(long[] ids)
    {
        var validIds = new List<long>();
        var results = await Task.WhenAll(ids.Select(i => CheckValidIdAsync(i)))
            .ConfigureAwait(false);
        for (int i = 0; i < ids.Length; i++)
        {
            if (results[i])
            {
                validIds.Add(ids[i]);
            }
        }
        return validIds;
    }

    private async Task<IEnumerable<long>> GetValidIds2(long[] ids)
    {
        var validIds = new ConcurrentBag<long>();
        await Task.WhenAll(ids.Select(async i =>
        {
            var valid = await CheckValidIdAsync(i);
            if (valid)
                validIds.Add(i);
        })).ConfigureAwait(false);
        return validIds;
    }

    private async Task<bool> CheckValidIdAsync(long id);

I currently use GetValidIds1() but it has inconvenience of having to tie input ids to result using index at the end.

GetValidIds2() is what i want to write but there are a few concerns:

  1. I have 'await' in select lambda expression. Because LINQ is lazy evaluation, I don't think it would block other CheckValidIdAsync() calls from starting but exactly who's context does it suspend? Per MSDN doc

The await operator suspends evaluation of the enclosing async method until the asynchronous operation represented by its operand completes.

So in this case, the enclosing async method is lambda expression itself so it doesn't affect other calls?

  1. Is there a better way to process result of async method and collect output of that process in a list?

Solution

  • Another way to do it is to project each long ID to a Task<ValueTuple<long, bool>>, instead of projecting it to a Task<bool>. This way you'll be able to filter the results using pure LINQ:

    private async Task<long[]> GetValidIds3(long[] ids)
    {
        IEnumerable<Task<(long Id, bool IsValid)>> tasks = ids
            .Select(async id =>
            {
                bool isValid = await CheckValidIdAsync(id).ConfigureAwait(false);
                return (id, isValid);
            });
        var results = await Task.WhenAll(tasks).ConfigureAwait(false);
        return results
            .Where(e => e.IsValid)
            .Select(e => e.Id)
            .ToArray();
    }
    

    The above GetValidIds3 is equivalent with the GetValidIds1 in your question. It returns the filtered IDs in the same order as the original ids. On the contrary the GetValidIds2 doesn't guarantee any order. If you have to use a concurrent collection, it's better to use a ConcurrentQueue<T> instead of a ConcurrentBag<T>, because the former preserves the insertion order. Even if the order is not important, preserving it makes the debugging easier.