I know this has been done many times before, but this is my existing code taken from a sample I found to async process a list of tasks. I've seen Task.Run in use before in many examples, but I'm assuming that in most cases, the workload will be IO bound and therefore the Task scheduler is not clever enough to know whether to grab a thread pool thread rather than use TaskCompletionSource.
I also wanted the below extension method to fast fail, that is if there are 50 items being processed I want the whole thing to fail if just one task encounters an exception. I use a Task.Delay as a "marker" task to signal the whole batch to finish. Is the below a good implementation and if so how can it be improved upon?
public static async Task ForEachAsyncSemaphore<T>(this IEnumerable<T> items,
Func<T, Task> func,
int dop,
CancellationToken ct)
{
Exception capturedException = null;
var childCts = new CancellationTokenSource();
var linkedTokens = CancellationTokenSource.CreateLinkedTokenSource(ct, childCts.Token);
using (var semaphore = new SemaphoreSlim(dop))
{
var cancellationTask = Task.Delay(Timeout.Infinite, linkedTokens.Token);
var tasks = items.Select(async item =>
{
await semaphore.WaitAsync(linkedTokens.Token);
try
{
await func(item);
}
catch (Exception ex)
{
capturedException = ex;
childCts.Cancel();
}
finally
{
semaphore.Release();
}
}).ToArray();
// Use cancellation task as signal to exit early if unexpected exception
await Task.WhenAny(Task.WhenAll(tasks), cancellationTask);
if (capturedException != null)
{
throw capturedException;
}
}