CodingNeed.

Production engineering workshop · 45 MIN

C# · Bound asynchronous work and propagate cancellation

Bound concurrency independently from the size of the input.

Parallel.ForEachAsync limits simultaneous delegate executions. CancellationToken must be passed into the delegate’s asynchronous operation; cancellation is cooperative. Store each result at its input index to preserve order without a shared append operation. Exceptions cause the returned task to fail; decide explicitly whether partial results are useful. This implementation expects a materialized array, so it does not claim constant-memory streaming.

First reproduce the normal path. Then force a failure at each boundary and inspect what remains true.

Read the example

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

public static class Pipeline {
  public static async Task<int[]> MapAsync(
      int[] input, int concurrency,
      Func<int,CancellationToken,Task<int>> transform,
      CancellationToken ct) {
    if (concurrency < 1) throw new ArgumentOutOfRangeException(nameof(concurrency));
    var result = new int[input.Length];
    await Parallel.ForEachAsync(Enumerable.Range(0,input.Length),
      new ParallelOptions { MaxDegreeOfParallelism=concurrency, CancellationToken=ct },
      async (index, token) => { result[index] = await transform(input[index], token); });
    return result;
  }
}
// Example inside async Main:
// var result = await Pipeline.MapAsync(new[]{1,2,3}, 2,
//   async (n, ct) => { await Task.Delay(10,ct); return n*n; }, CancellationToken.None);
Check the expected output
Review the behavior in the stated project environment.

Your challenge

In a .NET console project, implement the function below and test an empty array, cancellation during Task.Delay, and a delegate that throws. Replace the sample transform with an HttpClient operation and propagate the supplied token. Use an injected managed HttpClient in a server project.

Solution cost: O(n) bookkeeping plus bounded asynchronous work. time · O(n + c) output and concurrency state. space

Common trap

Task.WhenAll over all input items does not itself bound the number of operations started.

Study the project implementation
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

public static class Pipeline {
  public static async Task<int[]> MapAsync(
      int[] input, int concurrency,
      Func<int,CancellationToken,Task<int>> transform,
      CancellationToken ct) {
    if (concurrency < 1) throw new ArgumentOutOfRangeException(nameof(concurrency));
    var result = new int[input.Length];
    await Parallel.ForEachAsync(Enumerable.Range(0,input.Length),
      new ParallelOptions { MaxDegreeOfParallelism=concurrency, CancellationToken=ct },
      async (index, token) => { result[index] = await transform(input[index], token); });
    return result;
  }
}
// Example inside async Main:
// var result = await Pipeline.MapAsync(new[]{1,2,3}, 2,
//   async (n, ct) => { await Task.Delay(10,ct); return n*n; }, CancellationToken.None);

Further reading: Official documentation