From fcfb0e80a039d8283f6a298ed67a26c23e3a560a Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Wed, 1 Apr 2026 23:34:16 +0200 Subject: [PATCH] revival of the async-io functions --- build.zig | 9 +++-- exercises/087_async4.zig | 58 +++++++++++++++++++---------- exercises/104_threading.zig | 53 +++++++++++--------------- patches/patches/087_async4.patch | 11 ++++++ patches/patches/104_threading.patch | 6 +-- 5 files changed, 81 insertions(+), 56 deletions(-) create mode 100644 patches/patches/087_async4.patch diff --git a/build.zig b/build.zig index d2ee3aa..1d40dec 100644 --- a/build.zig +++ b/build.zig @@ -1131,9 +1131,12 @@ const exercises = [_]Exercise{ }, .{ .main_file = "087_async4.zig", - .output = "1 2 3 4 5", - .skip = true, - .skip_hint = "async has not been implemented in the current compiler version.", + .output = + \\Task 1 done. + \\Task 2 done. + \\Task 3 done. + \\All tasks finished! + , // pay attention to the comma }, .{ .main_file = "088_async5.zig", diff --git a/exercises/087_async4.zig b/exercises/087_async4.zig index bb9c9ec..50829fc 100644 --- a/exercises/087_async4.zig +++ b/exercises/087_async4.zig @@ -1,30 +1,50 @@ // -// It has probably not escaped your attention that we are no -// longer capturing a return value from foo() because the 'async' -// keyword returns the frame instead. +// When you have many tasks that don't return individual values, +// use a Group! A Group is an unordered set of tasks that can +// only be awaited or canceled as a whole: // -// One way to solve this is to use a global variable. +// var group: std.Io.Group = .init; +// group.async(io, myTask, .{arg1}); +// group.async(io, myTask, .{arg2}); +// try group.await(io); // blocks until ALL tasks finish // -// See if you can make this program print "1 2 3 4 5". +// Important rules: +// * The return type of functions spawned in a group must be +// coercible to Cancelable!void (i.e. void, or error{Canceled}!void). +// * Once you call group.async(), you MUST eventually call +// group.await() or group.cancel() to release resources. +// * group.cancel() requests cancellation on ALL members, +// then waits for them to finish. // -const print = @import("std").debug.print; +// Unlike Future, Group tasks don't return values to the caller. +// They're ideal for parallel work that communicates through +// shared state or side effects (like printing). +// +// Fix this program to await all tasks in the group. +// +const std = @import("std"); +const print = std.debug.print; -var global_counter: i32 = 0; +pub fn main(init: std.process.Init) !void { + const io = init.io; -pub fn main() void { - var foo_frame = async foo(); + var group: std.Io.Group = .init; - while (global_counter <= 5) { - print("{} ", .{global_counter}); - ??? - } + // Spawn 3 tasks in any order. Each sleeps for (id * 1) seconds + // before printing, so the output order is deterministic. + group.async(io, doWork, .{ io, 1 }); + group.async(io, doWork, .{ io, 3 }); + group.async(io, doWork, .{ io, 2 }); - print("\n", .{}); + // Wait for all tasks to finish. + // What Group method blocks until all tasks complete? + try group.??? + + print("All tasks finished!\n", .{}); } -fn foo() void { - while (true) { - ??? - ??? - } +fn doWork(io: std.Io, id: u32) void { + // Sleep ensures deterministic output order. + io.sleep(std.Io.Duration.fromSeconds(id), .awake) catch return; + print("Task {} done.\n", .{id}); } diff --git a/exercises/104_threading.zig b/exercises/104_threading.zig index 2b0e6f7..3c3fa21 100644 --- a/exercises/104_threading.zig +++ b/exercises/104_threading.zig @@ -1,31 +1,22 @@ // -// Whenever there is a lot to calculate, the question arises as to how -// tasks can be carried out simultaneously. We have already learned about -// one possibility, namely asynchronous processes, in Exercises 84-91. +// In Exercises 84-91, we learned about Zig's Io interface for +// concurrent execution: io.async(), Group, Select, and Futures. +// Under the hood, the Threaded backend manages a pool of real +// OS threads for you - including scheduling, cancellation, and +// resource cleanup. // -// However, the computing power of the processor is only distributed to -// the started and running tasks, which always reaches its limits when -// pure computing power is called up. +// But sometimes you need direct control over threads: +// * Long-lived dedicated workers +// * Specific stack sizes or thread counts +// * Code that doesn't have an Io interface available +// * Fine-grained synchronization patterns // -// For example, in blockchains based on proof of work, the miners have -// to find a nonce for a certain character string so that the first m bits -// in the hash of the character string and the nonce are zeros. -// As the miner who can solve the task first receives the reward, everyone -// tries to complete the calculations as quickly as possible. +// That's where std.Thread comes in. It gives you a raw OS thread +// that you spawn, manage, and join yourself. No pool, no Futures, +// no automatic cancellation - but full control. // -// This is where multithreading comes into play, where tasks are actually -// distributed across several cores of the CPU or GPU, which then really -// means a multiplication of performance. -// -// The following diagram roughly illustrates the difference between the -// various types of process execution. -// The 'Overall Time' column is intended to illustrate how the time is -// affected if, instead of one core as in synchronous and asynchronous -// processing, a second core now helps to complete the work in multithreading. -// -// In the ideal case shown, execution takes only half the time compared -// to the synchronous single thread. And even asynchronous processing -// is only slightly faster in comparison. +// The following diagram roughly illustrates the difference between +// the various types of process execution: // // // Synchronous Asynchronous @@ -108,7 +99,7 @@ pub fn main() !void { // they run in parallel and we can still do some work in between. var io_instance: std.Io.Threaded = .init_single_threaded; const io = io_instance.io(); - try io.sleep(std.Io.Duration.fromSeconds(4), .awake); + try io.sleep(std.Io.Duration.fromMilliseconds(400), .awake); std.debug.print("Some weird stuff, after starting the threads.\n", .{}); } // After we have left the closed area, we wait until @@ -118,17 +109,17 @@ pub fn main() !void { // This function is started with every thread that we set up. // In our example, we pass the number of the thread as a parameter. -fn thread_function(num: usize) !void { +fn thread_function(id: usize) !void { var io_instance: std.Io.Threaded = .init_single_threaded; const io = io_instance.io(); - try io.sleep(std.Io.Duration.fromSeconds(1 * @as(isize, @intCast(num))), .awake); - std.debug.print("thread {d}: {s}\n", .{ num, "started." }); + try io.sleep(std.Io.Duration.fromMilliseconds(100 * @as(isize, @intCast(id))), .awake); + std.debug.print("thread {d}: {s}\n", .{ id, "started." }); // This timer simulates the work of the thread. - const work_time = 3 * ((5 - num % 3) - 2); - try io.sleep(std.Io.Duration.fromSeconds(@intCast(work_time)), .awake); + const work_time = 300 * ((5 - id % 3) - 2); + try io.sleep(std.Io.Duration.fromMilliseconds(@intCast(work_time)), .awake); - std.debug.print("thread {d}: {s}\n", .{ num, "finished." }); + std.debug.print("thread {d}: {s}\n", .{ id, "finished." }); } // This is the easiest way to run threads in parallel. // In general, however, more management effort is required, diff --git a/patches/patches/087_async4.patch b/patches/patches/087_async4.patch new file mode 100644 index 0000000..4883afd --- /dev/null +++ b/patches/patches/087_async4.patch @@ -0,0 +1,11 @@ +--- exercises/087_async4.zig 2026-04-01 23:17:31.066443941 +0200 ++++ answers/087_async4.zig 2026-04-01 23:17:39.251612131 +0200 +@@ -38,7 +38,7 @@ + + // Wait for all tasks to finish. + // What Group method blocks until all tasks complete? +- try group.??? ++ try group.await(io); + + print("All tasks finished!\n", .{}); + } diff --git a/patches/patches/104_threading.patch b/patches/patches/104_threading.patch index eda25fd..e6fe0f4 100644 --- a/patches/patches/104_threading.patch +++ b/patches/patches/104_threading.patch @@ -1,6 +1,6 @@ ---- exercises/104_threading.zig 2025-11-28 14:17:31.552529679 +0100 -+++ answers/104_threading.zig 2025-11-28 14:15:36.823931851 +0100 -@@ -97,12 +97,12 @@ +--- exercises/104_threading.zig 2026-04-01 23:31:10.073198955 +0200 ++++ answers/104_threading.zig 2026-04-01 23:29:51.314585919 +0200 +@@ -88,12 +88,12 @@ defer handle.join(); // Second thread