| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Process Multiple Asynchronous Tasks in Various Ways - One at a time / Batched / Rate limited / Concurrently
Install via Nuget Install-Package EnumerableAsyncProcessor
See the benchmark guide to run the BenchmarkDotNet suite, filter scenarios, and compare revisions.
Version 4 requires .NET 8 or later. The package targets and tests net8.0, net9.0, and net10.0.
Version 4 is a major release. Review these source and behaviour changes before upgrading:
Version 4 also adds cancellation-aware selectors. Use (item, cancellationToken) => ... when in-flight work must observe external cancellation, CancelAll(), or disposal:
await using var processor = ids
.ForEachAsync(
async (id, cancellationToken) =>
await DoSomethingAsync(id, cancellationToken),
cancellationToken)
.ProcessInParallel(maxConcurrency: 100);
await processor.WaitAsync();Bounded parallel processors use a fixed set of workers, so coordination work scales with maxConcurrency instead of item count. Bounded IAsyncEnumerable<T> processing uses a bounded channel to apply source backpressure while preserving result order. Unbounded processing starts work as input is consumed and can place substantial pressure on memory, CPU, network connections, or downstream services.
Timed processors acquire a shared token-bucket permit before starting each operation. The permit rate and maximum in-flight concurrency are separate controls; long-running operations therefore do not reduce permit replenishment. A zero-length window disables start-rate throttling but retains the concurrency limit.
Because I've come across situations where you need to fine tune the rate at which you do things. Maybe you want it fast. Maybe you want it slow. Maybe you want it at a safe balance. Maybe you just don't want to write all the boilerplate code that comes with managing asynchronous operations!
Types
| Type | Source Object | Return Object | Method 1 | Method 2 |
|---|---|---|---|---|
| ParallelAsyncProcessor | ❌ | ❌ | .WithExecutionCount(int) | .ForEachAsync(delegate) |
| ParallelAsyncProcessor<TInput> | ✔ | ❌ | .WithItems(IEnumerable<TInput>) | .ForEachAsync(delegate) |
| ResultParallelAsyncProcessor<TOutput> | ❌ | ✔ | .WithExecutionCount(int) | .SelectAsync(delegate) |
| ResultParallelAsyncProcessor<TInput, TOutput> | ✔ | ✔ | .WithItems(IEnumerable<TInput>) | .SelectAsync(delegate) |
How it works
Processes asynchronous tasks in parallel. Pass maxConcurrency to use a fixed worker pool and bound the number of operations running at once, or omit it for unbounded concurrency.
Usage
var ids = Enumerable.Range(0, 5000).ToList();
// SelectAsync for if you want to return something - using proper disposal
await using var processor = ids
.SelectAsync(id => DoSomethingAndReturnSomethingAsync(id), CancellationToken.None)
.ProcessInParallel(maxConcurrency: 100);
var results = await processor.GetResultsAsync();
// ForEachAsync for when you have nothing to return - using proper disposal
await using var voidProcessor = ids
.ForEachAsync(id => DoSomethingAsync(id), CancellationToken.None)
.ProcessInParallel(maxConcurrency: 100);
await voidProcessor.WaitAsync();
// Omit maxConcurrency for unbounded parallel processing
await using var unboundedProcessor = ids
.ForEachAsync(id => DoSomethingAsync(id), CancellationToken.None)
.ProcessInParallel();
await unboundedProcessor.WaitAsync();Choose a concurrency limit that protects downstream resources. Unbounded processing can increase memory, CPU, and network pressure.
Types
| Type | Source Object | Return Object | Method 1 | Method 2 |
|---|---|---|---|---|
| TimedRateLimitedParallelAsyncProcessor | ❌ | ❌ | .WithExecutionCount(int) | .ForEachAsync(delegate) |
| TimedRateLimitedParallelAsyncProcessor<TInput> | ✔ | ❌ | .WithItems(IEnumerable<TInput>) | .ForEachAsync(delegate) |
| ResultTimedRateLimitedParallelAsyncProcessor<TOutput> | ❌ | ✔ | .WithExecutionCount(int) | .SelectAsync(delegate) |
| ResultTimedRateLimitedParallelAsyncProcessor<TInput, TOutput> | ✔ | ✔ | .WithItems(IEnumerable<TInput>) | .SelectAsync(delegate) |
How it works
Uses a shared token bucket to limit how many operations may start in each window. This is useful when a downstream API has a requests-per-second limit.
The two-argument overload uses the same value for permits per window and maximum in-flight concurrency. Use the three-argument overload when those limits should differ.
Usage
var ids = Enumerable.Range(0, 5000).ToList();
// SelectAsync for if you want to return something - using proper disposal
await using var processor = ids
.SelectAsync(id => DoSomethingAndReturnSomethingAsync(id), CancellationToken.None)
.ProcessInParallel(
permitsPerWindow: 100,
window: TimeSpan.FromSeconds(1),
maxConcurrency: 200);
var results = await processor.GetResultsAsync();
// ForEachAsync for when you have nothing to return - using proper disposal
await using var voidProcessor = ids
.ForEachAsync(id => DoSomethingAsync(id), CancellationToken.None)
.ProcessInParallel(maxConcurrency: 100, timeSpan: TimeSpan.FromSeconds(1));
await voidProcessor.WaitAsync();The first example allows up to 100 starts per second and 200 in-flight operations. The second preserves the source-compatible two-argument shape and applies 100 to both limits.
Types
| Type | Source Object | Return Object | Method 1 | Method 2 |
|---|---|---|---|---|
| OneAtATimeAsyncProcessor | ❌ | ❌ | .WithExecutionCount(int) | .ForEachAsync(delegate) |
| OneAtATimeAsyncProcessor<TInput> | ✔ | ❌ | .WithItems(IEnumerable<TInput>) | .ForEachAsync(delegate) |
| ResultOneAtATimeAsyncProcessor<TOutput> | ❌ | ✔ | .WithExecutionCount(int) | .SelectAsync(delegate) |
| ResultOneAtATimeAsyncProcessor<TInput, TOutput> | ✔ | ✔ | .WithItems(IEnumerable<TInput>) | .SelectAsync(delegate) |
How it works
Processes your Asynchronous Tasks One at a Time. Only one will ever progress at a time. As one finishes, another will start
Usage
var ids = Enumerable.Range(0, 5000).ToList();
// SelectAsync for if you want to return something
var results = await ids
.SelectAsync(id => DoSomethingAndReturnSomethingAsync(id), CancellationToken.None)
.ProcessOneAtATime();
// ForEachAsync for when you have nothing to return
await ids
.ForEachAsync(id => DoSomethingAsync(id), CancellationToken.None)
.ProcessOneAtATime();Caveats
Types
| Type | Source Object | Return Object | Method 1 | Method 2 |
|---|---|---|---|---|
| BatchAsyncProcessor | ❌ | ❌ | .WithExecutionCount(int) | .ForEachAsync(delegate) |
| BatchAsyncProcessor<TInput> | ✔ | ❌ | .WithItems(IEnumerable<TInput>) | .ForEachAsync(delegate) |
| ResultBatchAsyncProcessor<TOutput> | ❌ | ✔ | .WithExecutionCount(int) | .SelectAsync(delegate) |
| ResultBatchAsyncProcessor<TInput, TOutput> | ✔ | ✔ | .WithItems(IEnumerable<TInput>) | .SelectAsync(delegate) |
How it works
Processes your Asynchronous Tasks in Batches. The next batch will not start until every Task in previous batch has finished
Usage
var ids = Enumerable.Range(0, 5000).ToList();
// SelectAsync for if you want to return something
var results = await ids
.SelectAsync(id => DoSomethingAndReturnSomethingAsync(id), CancellationToken.None)
.ProcessInBatches(batchSize: 100);
// ForEachAsync for when you have nothing to return
await ids
.ForEachAsync(id => DoSomethingAsync(id), CancellationToken.None)
.ProcessInBatches(batchSize: 100);Caveats
As above, you can see that you can just await on the processor to get the results. Below shows examples of using the processor object and the various methods available.
Use an item processor when each operation needs an input value, such as sending notifications to a set of IDs.
var httpClient = new HttpClient();
var ids = Enumerable.Range(0, 5000).ToList();
// This is for when you need to Enumerate through some objects and use them in your operations
var itemProcessor = Enumerable.Range(0, 5000).ToAsyncProcessorBuilder()
.SelectAsync(NotifyAsync)
.ProcessInParallel(100);
// Or
// var itemProcessor = AsyncProcessorBuilder.WithItems(ids)
// .SelectAsync(NotifyAsync, CancellationToken.None)
// .ProcessInParallel(100);
// GetEnumerableTasks() returns IEnumerable<Task<TOutput>> - These may have completed, or may still be waiting to finish.
var tasks = itemProcessor.GetEnumerableTasks();
// Or call GetResultsAsyncEnumerable() to get an IAsyncEnumerable<TOutput> so you can process them in real-time as they finish.
await foreach (var httpResponseMessage in itemProcessor.GetResultsAsyncEnumerable())
{
// Do something
}
// Or call GetResultsAsync() to get a Task<TOutput[]> that contains all of the finished results
var results = await itemProcessor.GetResultsAsync();
// My dummy method
Task<HttpResponseMessage> NotifyAsync(int id)
{
return httpClient.GetAsync($"https://localhost:8080/notify/{id}");
}Use an execution-count processor when an operation should run a fixed number of times without input values, such as warming multiple site instances.
var httpClient = new HttpClient();
var itemProcessor = AsyncProcessorBuilder.WithExecutionCount(100)
.SelectAsync(PingAsync, CancellationToken.None)
.ProcessInParallel(10);
// GetEnumerableTasks() returns IEnumerable<Task<TOutput>> - These may have completed, or may still be waiting to finish.
var tasks = itemProcessor.GetEnumerableTasks();
// Or call GetResultsAsyncEnumerable() to get an IAsyncEnumerable<TOutput> so you can process them in real-time as they finish.
await foreach (var httpResponseMessage in itemProcessor.GetResultsAsyncEnumerable())
{
// Do something
}
// Or call GetResultsAsync() to get a Task<TOutput[]> that contains all of the finished results
var results = await itemProcessor.GetResultsAsync();
// My dummy method
Task<HttpResponseMessage> PingAsync()
{
return httpClient.GetAsync("https://localhost:8080/ping");
}Important: All processor objects implement IDisposable and IAsyncDisposable and should be properly disposed to ensure clean resource cleanup and task cancellation.
When you create a processor (e.g., using ProcessInParallel(), ProcessOneAtATime(), etc.), the processor:
Proper disposal ensures:
private static async IAsyncEnumerable<int> ProcessDataAsync(int[] input, CancellationToken token)
{
await using var processor = input
.SelectAsync(async x => await TransformAsync(x), token)
.ProcessInParallel();
await foreach (var result in processor.GetResultsAsyncEnumerable())
{
yield return result;
}
// Processor automatically disposed here
}private static async Task<int[]> ProcessDataAsync(int[] input, CancellationToken token)
{
using var processor = input
.SelectAsync(async x => await TransformAsync(x), token)
.ProcessInParallel();
return await processor.GetResultsAsync();
// Processor automatically disposed here
}private static async Task<int[]> ProcessDataAsync(int[] input, CancellationToken token)
{
var processor = input
.SelectAsync(async x => await TransformAsync(x), token)
.ProcessInParallel();
try
{
return await processor.GetResultsAsync();
}
finally
{
await processor.DisposeAsync();
}
}private static void StartProcessing(int[] input, CancellationToken token)
{
var processor = input
.SelectAsync(async x => await TransformAsync(x), token)
.ProcessInParallel();
// Don't wait for completion but ensure disposal
_ = Task.Run(async () =>
{
try
{
await processor.GetResultsAsync();
}
finally
{
await processor.DisposeAsync();
}
}, token);
}When a processor is disposed:
Note that the convenience extension methods like IAsyncEnumerable<T>.ProcessInParallel(selector) handle disposal automatically and return the final results directly:
// These extension methods handle disposal internally
var transformedResults = await asyncEnumerable.ProcessInParallel(async x => await TransformAsync(x));Processors built from an IAsyncEnumerable<T> source (IAsyncEnumerableProcessor) also dispose their internal resources automatically when ExecuteAsync completes; await using is still supported and safe if ExecuteAsync is never called.
The disposal guidance above applies when you're working with the processor objects directly (using the builder pattern).
// DON'T DO THIS - Never disposed!
var processor = input.SelectAsync(transform, token).ProcessInParallel();
return processor.GetResultsAsyncEnumerable();await using var processor = input.SelectAsync(transform, token).ProcessInParallel();
return await processor.GetResultsAsync();var processor = input.SelectAsync(transform, token).ProcessInParallel();
try {
return await processor.GetResultsAsync();
} finally {
await processor.DisposeAsync();
}// These handle disposal internally
var results = await asyncEnumerable.ProcessInParallel(transform);| Back | FazBrowse Home | New Git URL |