// Exercise 3: bounded concurrency + exception aggregation (hard) // // Implement ProcessAllAsync(items, maxConcurrency, worker): // - runs worker(item) for every item // - never has more than `maxConcurrency` workers in flight at once // (use SemaphoreSlim: WaitAsync before starting, Release in finally) // - returns results in the SAME ORDER as `items` // - if any worker throws, the method must still let every started worker // finish, then throw an AggregateException containing ALL failures. // Note: `await Task.WhenAll(...)` rethrows only the FIRST exception — // you need the WhenAll task's .Exception property to see them all. // // The self-check tracks the peak number of concurrently-running workers. // // Run with: dotnet run (inside a console project). using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; class Program { #pragma warning disable IDE0060, CS1998 static async Task ProcessAllAsync( IReadOnlyList items, int maxConcurrency, Func> worker) { // TODO: // using var gate = new SemaphoreSlim(maxConcurrency); // var tasks = items.Select(async item => { // await gate.WaitAsync(); // try { return await worker(item); } // finally { gate.Release(); } // }).ToArray(); // var all = Task.WhenAll(tasks); // try { return await all; } // catch { throw all.Exception!; } // every failure, not just the first return Array.Empty(); } #pragma warning restore IDE0060, CS1998 static async Task Main() { int pass = 0, total = 0; int running = 0, peak = 0; async Task Square(int x) { int now = Interlocked.Increment(ref running); InterlockedMax(ref peak, now); await Task.Delay(50 + (10 - x) * 5); // later items finish sooner Interlocked.Decrement(ref running); return x * x; } var input = Enumerable.Range(1, 10).ToArray(); var output = await ProcessAllAsync(input, 3, Square); total++; if (output.SequenceEqual(input.Select(x => x * x))) { Console.WriteLine("PASS: results in input order"); pass++; } else Console.WriteLine("FAIL: results " + string.Join(",", output)); total++; if (peak > 0 && peak <= 3) { Console.WriteLine($"PASS: peak concurrency {peak} <= 3"); pass++; } else Console.WriteLine($"FAIL: peak concurrency {peak}"); total++; try { await ProcessAllAsync(new[] { 1, 2, 3, 4 }, 2, async x => { await Task.Delay(10); if (x % 2 == 0) throw new InvalidOperationException("bad " + x); return x; }); Console.WriteLine("FAIL: no exception thrown"); } catch (AggregateException agg) { var msgs = agg.Flatten().InnerExceptions.Select(e => e.Message).OrderBy(m => m).ToArray(); if (msgs.SequenceEqual(new[] { "bad 2", "bad 4" })) { Console.WriteLine("PASS: all failures aggregated"); pass++; } else Console.WriteLine("FAIL: aggregated " + string.Join(";", msgs)); } catch (Exception e) { Console.WriteLine("FAIL: expected AggregateException, got " + e.GetType().Name); } Console.WriteLine($"--- {pass}/{total} checks passed ---"); } static void InterlockedMax(ref int target, int value) { int current; while ((current = Volatile.Read(ref target)) < value && Interlocked.CompareExchange(ref target, value, current) != current) { } } }