Index · How it works
1 min readRapid overview
- How it works
- What is a Coroutine?
- Key Characteristics
- Iterator-Based Coroutines (yield return)
- Basic Iterator Example
- yield break
- Async Iterators (IAsyncEnumerable)
- Basic Async Iterator
- Cancellation Support
- ConfigureAwait in Async Iterators
- Channels for Producer-Consumer
- Basic Channel Usage
- Bounded Channels with Backpressure
- Multiple Consumers
- Coroutine Patterns
- Pipeline Pattern
- Batching Pattern
- Merge Pattern
- State Machine Behind Iterators
- What the Compiler Generates
- Performance Considerations
- Avoid Allocations in Hot Paths
- Use ConfigureAwait(false)
- Channel Performance Tips
- Exercises
How it works
What is a Coroutine?
A coroutine is a generalization of a subroutine that allows multiple entry points for suspending and resuming execution at certain locations. Unlike regular functions that run to completion, coroutines can pause mid-execution and yield control back to the caller.
Key Characteristics
- Suspend and Resume: Can pause execution and continue later
- State Preservation: Maintains local variables between suspensions
- Cooperative Multitasking: Yields control voluntarily (not preemptively)
- Lazy Evaluation: Produces values on demand
Iterator-Based Coroutines (yield return)
The yield return statement creates an iterator that produces values lazily.
Basic Iterator Example
public static IEnumerable<int> GenerateNumbers(int count)
{
for (int i = 0; i < count; i++)
{
Console.WriteLine($"Generating {i}");
yield return i; // Suspend here, return value
}
Console.WriteLine("Done generating");
}
// Usage - lazy evaluation
foreach (var num in GenerateNumbers(5))
{
Console.WriteLine($"Received {num}");
if (num == 2) break; // Only generates 0, 1, 2
}
yield break
Use yield break to end iteration early:
public static IEnumerable<int> TakeWhilePositive(IEnumerable<int> source)
{
foreach (var item in source)
{
if (item < 0)
yield break; // Stop iteration
yield return item;
}
}
Async Iterators (IAsyncEnumerable)
C# 8.0 introduced IAsyncEnumerable<T> for asynchronous streaming of data.
Basic Async Iterator
public static async IAsyncEnumerable<int> FetchDataAsync(int count)
{
for (int i = 0; i < count; i++)
{
await Task.Delay(100); // Simulate async work
yield return i;
}
}
// Consumption
await foreach (var item in FetchDataAsync(10))
{
Console.WriteLine(item);
}
Cancellation Support
public static async IAsyncEnumerable<int> StreamWithCancellation(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
int i = 0;
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(100, cancellationToken);
yield return i++;
}
}
// Usage with cancellation
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await foreach (var item in StreamWithCancellation(cts.Token))
{
Console.WriteLine(item);
}
ConfigureAwait in Async Iterators
await foreach (var item in FetchDataAsync(10).ConfigureAwait(false))
{
// Runs on thread pool thread after first await
ProcessItem(item);
}
Channels for Producer-Consumer
System.Threading.Channels provides high-performance async producer-consumer patterns.
Basic Channel Usage
var channel = Channel.CreateUnbounded<int>();
// Producer
async Task ProduceAsync(ChannelWriter<int> writer)
{
for (int i = 0; i < 100; i++)
{
await writer.WriteAsync(i);
await Task.Delay(10);
}
writer.Complete();
}
// Consumer
async Task ConsumeAsync(ChannelReader<int> reader)
{
await foreach (var item in reader.ReadAllAsync())
{
Console.WriteLine($"Consumed: {item}");
}
}
// Run both
await Task.WhenAll(
ProduceAsync(channel.Writer),
ConsumeAsync(channel.Reader)
);
Bounded Channels with Backpressure
var options = new BoundedChannelOptions(10)
{
FullMode = BoundedChannelFullMode.Wait // Block when full
};
var channel = Channel.CreateBounded<WorkItem>(options);
// Producer will wait if channel is full
await channel.Writer.WriteAsync(new WorkItem());
Multiple Consumers
var channel = Channel.CreateUnbounded<int>();
// Start multiple consumers
var consumers = Enumerable.Range(0, 3)
.Select(id => Task.Run(async () =>
{
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine($"Consumer {id}: {item}");
}
}))
.ToArray();
// Producer writes, all consumers compete for items
for (int i = 0; i < 100; i++)
{
await channel.Writer.WriteAsync(i);
}
channel.Writer.Complete();
await Task.WhenAll(consumers);
Coroutine Patterns
Pipeline Pattern
public static async IAsyncEnumerable<TOutput> TransformAsync<TInput, TOutput>(
IAsyncEnumerable<TInput> source,
Func<TInput, Task<TOutput>> transformer,
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var item in source.WithCancellation(ct))
{
yield return await transformer(item);
}
}
// Build a pipeline
var numbers = GenerateAsync(100);
var doubled = TransformAsync(numbers, async x => x * 2);
var filtered = WhereAsync(doubled, async x => x > 50);
await foreach (var item in filtered)
{
Console.WriteLine(item);
}
Batching Pattern
public static async IAsyncEnumerable<IReadOnlyList<T>> BatchAsync<T>(
IAsyncEnumerable<T> source,
int batchSize,
[EnumeratorCancellation] CancellationToken ct = default)
{
var batch = new List<T>(batchSize);
await foreach (var item in source.WithCancellation(ct))
{
batch.Add(item);
if (batch.Count >= batchSize)
{
yield return batch;
batch = new List<T>(batchSize);
}
}
if (batch.Count > 0)
yield return batch;
}
Merge Pattern
public static async IAsyncEnumerable<T> MergeAsync<T>(
params IAsyncEnumerable<T>[] sources)
{
var channel = Channel.CreateUnbounded<T>();
var producers = sources.Select(async source =>
{
await foreach (var item in source)
{
await channel.Writer.WriteAsync(item);
}
}).ToArray();
_ = Task.WhenAll(producers).ContinueWith(_ => channel.Writer.Complete());
await foreach (var item in channel.Reader.ReadAllAsync())
{
yield return item;
}
}
State Machine Behind Iterators
The compiler transforms iterator methods into state machines.
What the Compiler Generates
// Your code
public IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}
// Compiler generates something like:
private sealed class GetNumbersIterator : IEnumerable<int>, IEnumerator<int>
{
private int state;
private int current;
public int Current => current;
public bool MoveNext()
{
switch (state)
{
case 0:
current = 1;
state = 1;
return true;
case 1:
current = 2;
state = 2;
return true;
case 2:
current = 3;
state = -1;
return true;
default:
return false;
}
}
}
Performance Considerations
Avoid Allocations in Hot Paths
// Bad - allocates iterator on each call
public IEnumerable<int> GetItems() => items.Where(x => x > 0);
// Better - use ValueTask for single values
public ValueTask<int> GetFirstAsync() =>
new ValueTask<int>(cachedValue);
Use ConfigureAwait(false)
public async IAsyncEnumerable<T> ProcessAsync<T>(
IAsyncEnumerable<T> source,
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var item in source.WithCancellation(ct).ConfigureAwait(false))
{
yield return await TransformAsync(item).ConfigureAwait(false);
}
}
Channel Performance Tips
// Use SingleReader/SingleWriter when applicable
var options = new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = true
};
var channel = Channel.CreateUnbounded<int>(options);
Exercises
Q: Implement an async iterator that reads lines from a file
Q: Create a debounce operator using channels
Q: Implement a rate limiter using IAsyncEnumerable
Q: Convert a callback-based API to IAsyncEnumerable