From 1c6487c1e79cbe0d59a39b483af8ec44b59c586e Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Fri, 3 Apr 2026 18:11:00 +0200 Subject: added async-io quiz --- exercises/095_quiz_async.zig | 186 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 exercises/095_quiz_async.zig (limited to 'exercises/095_quiz_async.zig') diff --git a/exercises/095_quiz_async.zig b/exercises/095_quiz_async.zig new file mode 100644 index 0000000..fb78e7b --- /dev/null +++ b/exercises/095_quiz_async.zig @@ -0,0 +1,186 @@ +// +// Quiz Time — Async I/O! +// +// Doctor Zoraptera's insect simulation is going well, but she +// realized that her virtual garden needs weather data! Insects +// behave differently depending on temperature, humidity, and +// wind conditions. +// +// She has set up three weather sensors around the garden that +// measure conditions in parallel and report their readings +// through a shared data channel. A collector task gathers the +// readings, and after all sensors have reported, a garden +// report is printed. +// +// But Doctor Z rushed through the code (she was being chased +// by a grasshopper) and left several bugs. Can you fix them? +// +// Here's what the program should do: +// 1. Three sensor tasks run concurrently, each sending +// exactly 3 readings through a Queue +// 2. A collector task receives readings, protected by a Mutex +// 3. After all sensors finish, the queue is closed +// 4. The final report is written in a cancel-protected section +// +// ************************************************************* +// * A NOTE ABOUT THIS EXERCISE * +// * * +// * This quiz uses concepts from exercises 084-093. * +// * There are 6 bugs to fix — look for the ???s! * +// * * +// ************************************************************* +// +const std = @import("std"); +const print = std.debug.print; + +const SensorType = enum { thermometer, hygrometer, anemometer }; + +const Reading = struct { + sensor_type: SensorType, + value: i32, +}; + +const GardenWeather = struct { + temperature: i32 = 0, + humidity: i32 = 0, + wind: i32 = 0, + readings_count: u32 = 0, + mutex: std.Io.Mutex = .init, + + fn addReading(self: *GardenWeather, io: std.Io, reading: Reading) void { + // Bug 1: The collector needs to lock before modifying + // shared state. What Mutex method acquires the lock? + self.mutex.lock(io) catch return; + self.mutex.???(io) catch return; + + switch (reading.sensor_type) { + .thermometer => self.temperature = reading.value, + .hygrometer => self.humidity = reading.value, + .anemometer => self.wind = reading.value, + } + self.readings_count += 1; + } +}; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var weather = GardenWeather{}; + + var reading_buf: [8]Reading = undefined; + var queue: std.Io.Queue(Reading) = .init(&reading_buf); + + // Sensor group: runs all three sensors to completion. + var sensors: std.Io.Group = .init; + + // Start three sensor tasks. They need GUARANTEED concurrency + // since they each simulate real-time measurement. + // + // Bug 2: io.async doesn't guarantee a separate thread. + // Which Io method guarantees true concurrency? + // (Don't forget: it can fail, so you need 'try'!) + try sensors.???(io, sensor, .{ io, &queue, .thermometer, 20 }); + try sensors.???(io, sensor, .{ io, &queue, .hygrometer, 60 }); + try sensors.???(io, sensor, .{ io, &queue, .anemometer, 10 }); + + // Collector group: processes readings from the queue. + var collectors: std.Io.Group = .init; + collectors.async(io, collector, .{ io, &queue, &weather }); + + // Bug 3: Wait for ALL sensors to finish sending their readings. + // What Group method blocks until all tasks complete? + try sensors.await(io); + // try sensors.???(io); + + // All sensors done — close the queue so the collector knows + // there's no more data coming. + queue.close(io); + + // Wait for the collector to drain the queue. + try collectors.await(io); + + // Now write the garden report. This is critical — it must + // NOT be interrupted, even if something tries to cancel us! + // + // Bug 4: Protect this section from cancellation. + // What Io method swaps the cancel protection state? + const old_protection = io.???(.blocked); + defer _ = io.???(old_protection); + + printGardenReport(&weather); +} + +fn sensor( + io: std.Io, + queue: *std.Io.Queue(Reading), + sensor_type: SensorType, + base_value: i32, +) void { + // Each sensor takes exactly 3 measurements. + for (1..4) |i| { + io.sleep(std.Io.Duration.fromMilliseconds(100), .awake) catch return; + + const reading = Reading{ + .sensor_type = sensor_type, + .value = base_value + @as(i32, @intCast(i)), + }; + + // Bug 5: Send the reading into the queue. + // What Queue method sends a single element? + queue.???(io, reading) catch return; + } +} + +fn collector( + io: std.Io, + queue: *std.Io.Queue(Reading), + weather: *GardenWeather, +) void { + while (true) { + const reading = queue.getOne(io) catch |err| switch (err) { + error.Closed => break, + error.Canceled => return, + }; + weather.addReading(io, reading); + } +} + +fn printGardenReport(weather: *GardenWeather) void { + print("=== Doctor Zoraptera's Garden Report ===\n", .{}); + print("Temperature : {}C\n", .{weather.temperature}); + print("Humidity : {}%\n", .{weather.humidity}); + print("Wind : {} km/h\n", .{weather.wind}); + print("Readings : {}\n", .{weather.readings_count}); + + if (weather.temperature > 20 and weather.wind < 15) { + print("Bee-friendly conditions! Expect high pollination.\n", .{}); + } else { + print("Grasshoppers will be grumpy today.\n", .{}); + } +} + +// Further reading for the curious: +// +// This quiz covered the main async I/O primitives: +// io.async() - launch a task (may run inline) +// io.concurrent() - launch with guaranteed parallelism +// Group.concurrent() - concurrent tasks in a group +// Future.await/cancel - collect or cancel a single task +// Group.async/await/cancel - manage fire-and-forget tasks +// Select.async/await - race tasks, act on first completion +// Queue - bounded channel between tasks +// Mutex - protect shared state +// CancelProtection - shield critical sections +// +// There are more synchronization primitives we didn't cover: +// Condition - wait for a condition to become true +// RwLock - multiple readers OR one writer +// Semaphore - limit concurrent access to a resource +// Futex - low-level wait/wake on a memory address +// Batch - submit multiple I/O operations at once +// +// The key insight: all of these work through the Io VTable, +// so your code is portable across backends (Threaded, Uring, +// Kqueue, Dispatch) without any changes! +// +// Doctor Zoraptera approves. -- cgit v1.2.3 From 882c6aa0ab174f0d7a28a7804531611fcef98476 Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Mon, 6 Apr 2026 19:57:32 +0200 Subject: improvements for async-io --- exercises/095_quiz_async.zig | 60 +++++++++++++++++++----------------- patches/patches/095_quiz_async.patch | 54 +++++++++++++++++--------------- 2 files changed, 60 insertions(+), 54 deletions(-) (limited to 'exercises/095_quiz_async.zig') diff --git a/exercises/095_quiz_async.zig b/exercises/095_quiz_async.zig index fb78e7b..b116fc9 100644 --- a/exercises/095_quiz_async.zig +++ b/exercises/095_quiz_async.zig @@ -16,16 +16,17 @@ // by a grasshopper) and left several bugs. Can you fix them? // // Here's what the program should do: -// 1. Three sensor tasks run concurrently, each sending -// exactly 3 readings through a Queue -// 2. A collector task receives readings, protected by a Mutex +// 1. Three sensor tasks send exactly 3 readings each through +// a Queue +// 2. A collector task receives readings concurrently, +// protected by a Mutex // 3. After all sensors finish, the queue is closed // 4. The final report is written in a cancel-protected section // // ************************************************************* // * A NOTE ABOUT THIS EXERCISE * // * * -// * This quiz uses concepts from exercises 084-093. * +// * This quiz uses concepts from exercises 085-094. * // * There are 6 bugs to fix — look for the ???s! * // * * // ************************************************************* @@ -50,8 +51,8 @@ const GardenWeather = struct { fn addReading(self: *GardenWeather, io: std.Io, reading: Reading) void { // Bug 1: The collector needs to lock before modifying // shared state. What Mutex method acquires the lock? - self.mutex.lock(io) catch return; self.mutex.???(io) catch return; + defer self.mutex.unlock(io); switch (reading.sensor_type) { .thermometer => self.temperature = reading.value, @@ -70,39 +71,40 @@ pub fn main(init: std.process.Init) !void { var reading_buf: [8]Reading = undefined; var queue: std.Io.Queue(Reading) = .init(&reading_buf); - // Sensor group: runs all three sensors to completion. - var sensors: std.Io.Group = .init; - - // Start three sensor tasks. They need GUARANTEED concurrency - // since they each simulate real-time measurement. + // The collector must run concurrently so it can process + // readings while the sensors are still sending. + // Start it FIRST to ensure its concurrency unit is reserved. // - // Bug 2: io.async doesn't guarantee a separate thread. - // Which Io method guarantees true concurrency? - // (Don't forget: it can fail, so you need 'try'!) - try sensors.???(io, sensor, .{ io, &queue, .thermometer, 20 }); - try sensors.???(io, sensor, .{ io, &queue, .hygrometer, 60 }); - try sensors.???(io, sensor, .{ io, &queue, .anemometer, 10 }); + // Bug 2: The collector needs guaranteed concurrency. + // What method ensures a separate unit of concurrency? + // (Don't forget: it can fail!) + var collector_future = try io.???(collector, .{ io, &queue, &weather }); + defer _ = collector_future.cancel(io); + + // Sensor group: the sensors can use async — they just need + // to run, and async is more portable. + var sensors: std.Io.Group = .init; - // Collector group: processes readings from the queue. - var collectors: std.Io.Group = .init; - collectors.async(io, collector, .{ io, &queue, &weather }); + sensors.async(io, sensor, .{ io, &queue, .thermometer, 20 }); + sensors.async(io, sensor, .{ io, &queue, .hygrometer, 60 }); + sensors.async(io, sensor, .{ io, &queue, .anemometer, 10 }); // Bug 3: Wait for ALL sensors to finish sending their readings. // What Group method blocks until all tasks complete? - try sensors.await(io); - // try sensors.???(io); + try sensors.???(io); // All sensors done — close the queue so the collector knows // there's no more data coming. queue.close(io); - // Wait for the collector to drain the queue. - try collectors.await(io); + // Wait for the collector to drain the remaining queue. + _ = collector_future.await(io); + // _ = collector_future.???(io); // Now write the garden report. This is critical — it must // NOT be interrupted, even if something tries to cancel us! // - // Bug 4: Protect this section from cancellation. + // Bug 5: Protect this section from cancellation. // What Io method swaps the cancel protection state? const old_protection = io.???(.blocked); defer _ = io.???(old_protection); @@ -125,7 +127,7 @@ fn sensor( .value = base_value + @as(i32, @intCast(i)), }; - // Bug 5: Send the reading into the queue. + // Bug 6: Send the reading into the queue. // What Queue method sends a single element? queue.???(io, reading) catch return; } @@ -163,8 +165,7 @@ fn printGardenReport(weather: *GardenWeather) void { // // This quiz covered the main async I/O primitives: // io.async() - launch a task (may run inline) -// io.concurrent() - launch with guaranteed parallelism -// Group.concurrent() - concurrent tasks in a group +// io.concurrent() - guaranteed unit of concurrency // Future.await/cancel - collect or cancel a single task // Group.async/await/cancel - manage fire-and-forget tasks // Select.async/await - race tasks, act on first completion @@ -180,7 +181,8 @@ fn printGardenReport(weather: *GardenWeather) void { // Batch - submit multiple I/O operations at once // // The key insight: all of these work through the Io VTable, -// so your code is portable across backends (Threaded, Uring, -// Kqueue, Dispatch) without any changes! +// so your code is portable across backends — whether Threaded +// (OS thread pool), or Evented (M:N green threads / fibers +// that can provide concurrency even on a single OS thread). // // Doctor Zoraptera approves. diff --git a/patches/patches/095_quiz_async.patch b/patches/patches/095_quiz_async.patch index dbaae07..e3d2a79 100644 --- a/patches/patches/095_quiz_async.patch +++ b/patches/patches/095_quiz_async.patch @@ -1,38 +1,42 @@ ---- exercises/095_quiz_async.zig 2026-04-03 18:04:53.577391455 +0200 -+++ answers/095_quiz_async.zig 2026-04-03 18:05:42.570392172 +0200 +--- exercises/095_quiz_async.zig 2026-04-06 19:55:17.111817364 +0200 ++++ answers/095_quiz_async.zig 2026-04-06 19:56:16.063974543 +0200 @@ -51,7 +51,7 @@ + fn addReading(self: *GardenWeather, io: std.Io, reading: Reading) void { // Bug 1: The collector needs to lock before modifying // shared state. What Mutex method acquires the lock? - self.mutex.lock(io) catch return; - self.mutex.???(io) catch return; -+ defer self.mutex.unlock(io); ++ self.mutex.lock(io) catch return; + defer self.mutex.unlock(io); switch (reading.sensor_type) { - .thermometer => self.temperature = reading.value, -@@ -79,9 +79,9 @@ - // Bug 2: io.async doesn't guarantee a separate thread. - // Which Io method guarantees true concurrency? - // (Don't forget: it can fail, so you need 'try'!) -- try sensors.???(io, sensor, .{ io, &queue, .thermometer, 20 }); -- try sensors.???(io, sensor, .{ io, &queue, .hygrometer, 60 }); -- try sensors.???(io, sensor, .{ io, &queue, .anemometer, 10 }); -+ try sensors.concurrent(io, sensor, .{ io, &queue, .thermometer, 20 }); -+ try sensors.concurrent(io, sensor, .{ io, &queue, .hygrometer, 60 }); -+ try sensors.concurrent(io, sensor, .{ io, &queue, .anemometer, 10 }); - - // Collector group: processes readings from the queue. - var collectors: std.Io.Group = .init; -@@ -90,7 +90,6 @@ +@@ -78,7 +78,7 @@ + // Bug 2: The collector needs guaranteed concurrency. + // What method ensures a separate unit of concurrency? + // (Don't forget: it can fail!) +- var collector_future = try io.???(collector, .{ io, &queue, &weather }); ++ var collector_future = try io.concurrent(collector, .{ io, &queue, &weather }); + defer _ = collector_future.cancel(io); + + // Sensor group: the sensors can use async — they just need +@@ -91,7 +91,7 @@ + // Bug 3: Wait for ALL sensors to finish sending their readings. // What Group method blocks until all tasks complete? - try sensors.await(io); -- // try sensors.???(io); +- try sensors.???(io); ++ try sensors.await(io); // All sensors done — close the queue so the collector knows // there's no more data coming. -@@ -104,8 +103,8 @@ +@@ -99,15 +99,14 @@ + + // Wait for the collector to drain the remaining queue. + _ = collector_future.await(io); +- // _ = collector_future.???(io); + + // Now write the garden report. This is critical — it must + // NOT be interrupted, even if something tries to cancel us! // - // Bug 4: Protect this section from cancellation. + // Bug 5: Protect this section from cancellation. // What Io method swaps the cancel protection state? - const old_protection = io.???(.blocked); - defer _ = io.???(old_protection); @@ -41,9 +45,9 @@ printGardenReport(&weather); } -@@ -127,7 +126,7 @@ +@@ -129,7 +128,7 @@ - // Bug 5: Send the reading into the queue. + // Bug 6: Send the reading into the queue. // What Queue method sends a single element? - queue.???(io, reading) catch return; + queue.putOne(io, reading) catch return; -- cgit v1.2.3