// ============================================================================// AsyncLab – async / await in C#// ============================================================================// Goal: learn what async/await is mostly used for – waiting for I/O// (network, files, databases) WITHOUT blocking a thread – and how Task.Run// moves CPU-heavy work off the main thread.//// Setup: dotnet new console -n AsyncLab// replace Program.cs with this file, then: dotnet run//// The program starts a small local web server (LabServer, bottom of the// file) that simulates SLOW network services. No internet needed.//// Work through the TODOs in order. After every part: run the program,// choose the part in the menu and compare the times with the exercise sheet.// ============================================================================using System.Diagnostics;using System.Net;namespace AsyncLab;public static class Program{ // One shared HttpClient for the whole application (best practice!). // Relative URLs like "weather/Krems" are resolved against BaseAddress. private static readonly HttpClient Http = new() { BaseAddress = new Uri(LabServer.BaseUrl) }; private static readonly string[] Cities = { "Krems", "Wien", "Linz", "Graz", "Salzburg" }; public static async Task Main() { LabServer.Start(); while (true) { Console.WriteLine(); Console.WriteLine("===== AsyncLab ====="); Console.WriteLine(" 1 Sequential downloads (await one after another)"); Console.WriteLine(" 2 Concurrent downloads (Task.WhenAll)"); Console.WriteLine(" 3 Fastest mirror & timeout (Task.WhenAny, CancellationToken)"); Console.WriteLine(" 4 CPU-bound work (Task.Run, responsive UI)"); Console.WriteLine(" 5 Parallel speed-up (many Task.Run + Task.WhenAll)"); Console.WriteLine(" 0 Exit"); Console.Write("Choose a part: "); switch (Console.ReadLine()?.Trim()) { case "1": await Part1_SequentialAsync(); break; case "2": await Part2_ConcurrentAsync(); break; case "3": await Part3_WhenAnyAsync(); break; case "4": await Part4_CpuBoundAsync(); break; case "5": await Part5_ParallelAsync(); break; case "0": return; default: Console.WriteLine("Unknown option."); break; } } } // ------------------------------------------------------------------------ // PART 1 – Sequential downloads // Each request to /weather/{city} takes about 1 second. // ------------------------------------------------------------------------ private static async Task Part1_SequentialAsync() { Console.WriteLine("\n--- Part 1: sequential downloads ---"); var sw = Stopwatch.StartNew(); foreach (string city in Cities) { // TODO 1: Download the weather report for `city` from the URL // $"weather/{city}" using Http.GetStringAsync(...). // Await the result and store it in `report`. string report = "TODO 1"; Console.WriteLine($" [{sw.ElapsedMilliseconds,5} ms] {report}"); } Console.WriteLine($"Total time: {sw.ElapsedMilliseconds} ms"); } // ------------------------------------------------------------------------ // PART 2 – Concurrent downloads with Task.WhenAll // Same 5 requests – but this time we do not wait for each one before // starting the next. // ------------------------------------------------------------------------ private static async Task Part2_ConcurrentAsync() { Console.WriteLine("\n--- Part 2: concurrent downloads ---"); var sw = Stopwatch.StartNew(); // TODO 2a: For every city, START the download (Http.GetStringAsync) // but do NOT await it. Add the returned Task<string> to `tasks`. var tasks = new List<Task<string>>(); Console.WriteLine($" [{sw.ElapsedMilliseconds,5} ms] all {tasks.Count} requests started"); // TODO 2b: Wait for ALL tasks at once using Task.WhenAll(...). // Hint: awaiting Task.WhenAll on Task<string>s gives you a string[]. string[] reports = Array.Empty<string>(); foreach (string report in reports) Console.WriteLine($" {report}"); Console.WriteLine($"Total time: {sw.ElapsedMilliseconds} ms"); } // ------------------------------------------------------------------------ // PART 3 – Task.WhenAny: fastest mirror, timeout, cancellation // The same file is available on /mirror/1, /mirror/2 and /mirror/3, // but the mirrors respond at different speeds. /slow takes 5 seconds. // ------------------------------------------------------------------------ private static async Task Part3_WhenAnyAsync() { Console.WriteLine("\n--- Part 3a: fastest mirror ---"); var sw = Stopwatch.StartNew(); // TODO 3a: Start a download from each of the three mirrors // ($"mirror/{i}" for i = 1..3) and collect the tasks. // Use Task.WhenAny(...) to get the task that finishes FIRST, // then await that task to get its string. string fastest = "TODO 3a"; Console.WriteLine($" [{sw.ElapsedMilliseconds,5} ms] winner: {fastest}"); Console.WriteLine("\n--- Part 3b: timeout with Task.WhenAny ---"); sw.Restart(); // TODO 3b: Start a request to "slow" AND a Task.Delay(2000). // Race them with Task.WhenAny. If the delay task wins, // set `timedOut` to true. // Replace the placeholder below with your own slow request task. Task<string> slowTask = Task.FromResult("TODO 3b"); bool timedOut = false; Console.WriteLine(timedOut ? $" [{sw.ElapsedMilliseconds,5} ms] Timeout! The server took too long." : $" [{sw.ElapsedMilliseconds,5} ms] Answer: {await slowTask}"); Console.WriteLine("\n--- Part 3c (bonus): real cancellation with a CancellationToken ---"); sw.Restart(); // TODO 3c (BONUS): In 3b the slow request keeps running in the background. // Do it properly: create a CancellationTokenSource that cancels // after 2 seconds and pass its Token to Http.GetStringAsync("slow", token). // Catch the TaskCanceledException and print // "Request cancelled after timeout." together with the elapsed time. Console.WriteLine(" TODO 3c"); } // ------------------------------------------------------------------------ // PART 4 – CPU-bound work with Task.Run // CountPrimes(...) is pure computation – there is NOTHING to wait for, so // there is no "...Async" version. Task.Run moves it to a thread-pool thread. // ------------------------------------------------------------------------ private static async Task Part4_CpuBoundAsync() { Console.WriteLine("\n--- Part 4: CPU-bound work, UI stays responsive ---"); const int max = 4_000_000; var sw = Stopwatch.StartNew(); // TODO 4a: Run CountPrimes(2, max) in the background with Task.Run(...). // Replace the placeholder (which already "finished" with 0). Task<int> primeTask = Task.FromResult(0); // TODO 4b: While primeTask is NOT completed (primeTask.IsCompleted), // show a spinner ( | / - \ ) and wait 100 ms with // await Task.Delay(100) between the frames. // Tip: Console.Write("\r...") overwrites the current line. // TODO 4c: Await primeTask to get the result. int count = 0; Console.WriteLine($"\r Found {count} primes below {max:N0} in {sw.ElapsedMilliseconds} ms"); } // ------------------------------------------------------------------------ // PART 5 – Parallel speed-up with several Task.Run calls // ------------------------------------------------------------------------ private static async Task Part5_ParallelAsync() { Console.WriteLine("\n--- Part 5: parallel speed-up ---"); const int max = 8_000_000; Console.WriteLine($" This machine has {Environment.ProcessorCount} logical cores."); long baseline = 0; foreach (int taskCount in new[] { 1, 2, 4, Environment.ProcessorCount }.Distinct()) { var sw = Stopwatch.StartNew(); int total = await CountPrimesParallelAsync(max, taskCount); long ms = sw.ElapsedMilliseconds; if (taskCount == 1) baseline = ms; Console.WriteLine($" {taskCount,2} task(s): {total} primes in {ms,5} ms" + $" speed-up: {(double)baseline / Math.Max(ms, 1):F2}x"); } } private static async Task<int> CountPrimesParallelAsync(int max, int taskCount) { // TODO 5a: Split the range [0, max) into `taskCount` chunks of equal size. // For each chunk start Task.Run(() => CountPrimes(from, to)) // and add the task to `tasks`. // Careful 1: the LAST chunk must end exactly at `max`. // Careful 2: copy from/to into local variables INSIDE the loop // before using them in the lambda! var tasks = new List<Task<int>>(); // TODO 5b: Await all tasks with Task.WhenAll and return the SUM // of all partial results. await Task.CompletedTask; // remove this line when you are done return 0; } // ------------------------------------------------------------------------ // Helpers – no changes needed // ------------------------------------------------------------------------ /// <summary>Counts primes in [from, to). Pure CPU work, deliberately naive.</summary> private static int CountPrimes(int from, int to) { int count = 0; for (int n = Math.Max(from, 2); n < to; n++) { bool isPrime = true; for (int d = 2; (long)d * d <= n; d++) { if (n % d == 0) { isPrime = false; break; } } if (isPrime) count++; } return count; }}// ============================================================================// LabServer – a tiny local web server that simulates slow network services.// You do NOT need to change anything here (but it is worth a look: it is// itself written with async/await!).//// GET /weather/{city} ~1000 ms -> weather report for a city// GET /mirror/{1|2|3} 2500 / 800 / 1500 ms -> same file from 3 mirrors// GET /slow 5000 ms -> a server that is far too slow// ============================================================================public static class LabServer{ public const string BaseUrl = "http://localhost:5055/"; private static HttpListener? _listener; public static void Start() { _listener = new HttpListener(); _listener.Prefixes.Add(BaseUrl); _listener.Start(); _ = AcceptLoopAsync(); // runs in the background Console.WriteLine($"LabServer running at {BaseUrl}"); } private static async Task AcceptLoopAsync() { while (_listener is { IsListening: true }) { HttpListenerContext ctx; try { ctx = await _listener.GetContextAsync(); } catch { break; } _ = HandleAsync(ctx); // do not await -> many requests at the same time } } private static async Task HandleAsync(HttpListenerContext ctx) { string[] parts = ctx.Request.Url!.AbsolutePath.Trim('/').Split('/'); (int delayMs, string body) = parts switch { ["weather", var city] => (1000, Weather(Uri.UnescapeDataString(city))), ["mirror", "1"] => (2500, "data.zip from mirror 1 (Frankfurt)"), ["mirror", "2"] => (800, "data.zip from mirror 2 (Vienna)"), ["mirror", "3"] => (1500, "data.zip from mirror 3 (Zurich)"), ["slow"] => (5000, "finally... here is your answer"), _ => (0, "404 – unknown endpoint") }; try { await Task.Delay(delayMs); // simulated network / database latency byte[] bytes = System.Text.Encoding.UTF8.GetBytes(body); ctx.Response.ContentType = "text/plain; charset=utf-8"; ctx.Response.StatusCode = body.StartsWith("404") ? 404 : 200; await ctx.Response.OutputStream.WriteAsync(bytes); ctx.Response.Close(); } catch { // client gave up (e.g. cancellation) – ignore } } private static string Weather(string city) { string[] sky = { "sunny", "cloudy", "rainy", "foggy", "windy" }; int h = Math.Abs(city.Aggregate(17, (acc, c) => acc * 31 + c)); return $"{city,-9}: {h % 25 - 3,3} °C, {sky[h % sky.Length]}"; }}