summaryrefslogtreecommitdiff
path: root/exercises
diff options
context:
space:
mode:
authorChris Boesch <chrboesch@noreply.codeberg.org>2026-04-01 23:34:16 +0200
committerChris Boesch <chrboesch@noreply.codeberg.org>2026-04-01 23:34:16 +0200
commitfcfb0e80a039d8283f6a298ed67a26c23e3a560a (patch)
tree2987a0ce0114ce6898e98ada9356da2c462b3f4b /exercises
parentdb1fef8b864f1af6f9d2b8a7871e72d77747304d (diff)
revival of the async-io functions
Diffstat (limited to 'exercises')
-rw-r--r--exercises/087_async4.zig58
-rw-r--r--exercises/104_threading.zig53
2 files changed, 61 insertions, 50 deletions
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;
+
+pub fn main(init: std.process.Init) !void {
+ const io = init.io;
-var global_counter: i32 = 0;
+ var group: std.Io.Group = .init;
-pub fn main() void {
- var foo_frame = async foo();
+ // 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 });
- while (global_counter <= 5) {
- print("{} ", .{global_counter});
- ???
- }
+ // Wait for all tasks to finish.
+ // What Group method blocks until all tasks complete?
+ try group.???
- print("\n", .{});
+ 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,