C#
IAsyncEnumerable
async programming
parallel tasks
.NET

How to use C8 IAsyncEnumerableT to async-enumerate tasks run in parallel

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

IAsyncEnumerable<T> lets you yield results asynchronously, but it does not make work run in parallel by itself. To enumerate tasks as they finish, you need to start the tasks first, then await them in completion order and yield return each result from an async iterator.

IAsyncEnumerable<T> Is About Asynchronous Pull, Not Automatic Parallelism

An async stream is a good fit when consumers want to process results one at a time with await foreach. What it does not do automatically is launch multiple operations in parallel.

This is the key distinction:

  • 'IAsyncEnumerable<T> controls how results are delivered.'
  • 'Task controls the asynchronous operation itself.'
  • Parallelism happens only if you start multiple tasks before awaiting them.

Start the Tasks First

Suppose you want to fetch several URLs concurrently and consume results as soon as each request finishes.

csharp
1using System.Collections.Generic;
2using System.Linq;
3using System.Net.Http;
4using System.Threading.Tasks;
5
6static async Task<string> FetchAsync(HttpClient client, string url)
7{
8    return await client.GetStringAsync(url);
9}

Create all tasks up front:

csharp
var tasks = urls
    .Select(url => FetchAsync(client, url))
    .ToList();

At this point, the requests are in flight. Now you can expose their results through IAsyncEnumerable<string>.

Yield Results in Completion Order

A common pattern is Task.WhenAny inside an async iterator.

csharp
1using System.Collections.Generic;
2using System.Runtime.CompilerServices;
3using System.Threading;
4using System.Threading.Tasks;
5
6public static async IAsyncEnumerable<T> InCompletionOrder<T>(
7    IEnumerable<Task<T>> tasks,
8    [EnumeratorCancellation] CancellationToken cancellationToken = default)
9{
10    var pending = tasks.ToList();
11
12    while (pending.Count > 0)
13    {
14        cancellationToken.ThrowIfCancellationRequested();
15
16        Task<T> finished = await Task.WhenAny(pending);
17        pending.Remove(finished);
18
19        yield return await finished;
20    }
21}

You can consume it like this:

csharp
1await foreach (var html in InCompletionOrder(tasks, cancellationToken))
2{
3    Console.WriteLine($"Received payload of length {html.Length}");
4}

That gives you streaming consumption plus parallel task execution.

Why This Works

All tasks begin before the iterator starts waiting for completions. Task.WhenAny returns the first finished task, then the iterator yields that result immediately. The consumer does not have to wait for the slowest task before seeing any output.

This is different from Task.WhenAll, which waits for every task to finish before returning any results.

Bounded Concurrency Matters for Large Inputs

If urls contains thousands of items, starting every task at once can overwhelm the process or the remote service. In that case, use a concurrency limit.

A simple approach is SemaphoreSlim:

csharp
1using System.Collections.Generic;
2using System.Linq;
3using System.Threading;
4using System.Threading.Tasks;
5
6public static IEnumerable<Task<string>> StartWithLimit(
7    IEnumerable<string> urls,
8    HttpClient client,
9    int maxConcurrency)
10{
11    var gate = new SemaphoreSlim(maxConcurrency);
12
13    return urls.Select(async url =>
14    {
15        await gate.WaitAsync();
16        try
17        {
18            return await client.GetStringAsync(url);
19        }
20        finally
21        {
22            gate.Release();
23        }
24    }).ToList();
25}

Then feed those tasks into the async stream helper.

When to Use a Different Abstraction

If you only need “run all tasks and get all results,” then Task.WhenAll is simpler.

If you need a producer-consumer pipeline with backpressure, Channel<T> can be a better fit than IAsyncEnumerable<T>.

If you need data parallelism over a large source with built-in throttling, newer .NET APIs such as Parallel.ForEachAsync may express the workload more directly.

IAsyncEnumerable<T> is best when you want a clean streaming consumer API.

Common Pitfalls

A common mistake is creating each task inside the iterator loop and awaiting it immediately. That accidentally serializes the work.

Another pitfall is confusing completion order with input order. The Task.WhenAny pattern yields whichever task finishes first.

Be careful with exceptions too. If a task faults, await finished rethrows that exception when the iterator reaches that task.

Finally, unlimited concurrency is rarely a good production default. If the source is large, cap it.

Summary

  • 'IAsyncEnumerable<T> does not create parallelism on its own.'
  • Start multiple tasks first, then yield results as they complete.
  • Use Task.WhenAny inside an async iterator to stream completion-order results.
  • Use Task.WhenAll if you do not need incremental delivery.
  • Add bounded concurrency for large workloads to avoid resource spikes.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.