summaryrefslogtreecommitdiff
path: root/exercises/107_threading.zig
diff options
context:
space:
mode:
authorChris Boesch <chrboesch@noreply.codeberg.org>2026-04-03 19:32:53 +0200
committerChris Boesch <chrboesch@noreply.codeberg.org>2026-04-03 19:32:53 +0200
commit5307b2a338a92130bc498fb1dc7d21a9fd1b0db4 (patch)
tree51279ca4fbd7bd90294dd563640c12a8c25c79c6 /exercises/107_threading.zig
parent3056a2b5442f2f1ec58db3f3493109064ad2a2a5 (diff)
parentf6a6798c8b6b813bd2ceee81db276e05327a76e0 (diff)
Merge pull request 'revival of the async-io functions' (#383) from asyncIo into main
Reviewed-on: https://codeberg.org/ziglings/exercises/pulls/383
Diffstat (limited to 'exercises/107_threading.zig')
-rw-r--r--exercises/107_threading.zig129
1 files changed, 129 insertions, 0 deletions
diff --git a/exercises/107_threading.zig b/exercises/107_threading.zig
new file mode 100644
index 0000000..3c3fa21
--- /dev/null
+++ b/exercises/107_threading.zig
@@ -0,0 +1,129 @@
+//
+// 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.
+//
+// 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
+//
+// 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.
+//
+// The following diagram roughly illustrates the difference between
+// the various types of process execution:
+//
+//
+// Synchronous Asynchronous
+// Processing Processing Multithreading
+// ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
+// │ Thread 1 │ │ Thread 1 │ │ Thread 1 │ │ Thread 2 │
+// ├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤ Overall Time
+// └──┼┼┼┼┼───┴─┴──┼┼┼┼┼───┴──┴──┼┼┼┼┼───┴─┴──┼┼┼┼┼───┴──┬───────┬───────┬──
+// ├───┤ ├───┤ ├───┤ ├───┤ │ │ │
+// │ T │ │ T │ │ T │ │ T │ │ │ │
+// │ a │ │ a │ │ a │ │ a │ │ │ │
+// │ s │ │ s │ │ s │ │ s │ │ │ │
+// │ k │ │ k │ │ k │ │ k │ │ │ │
+// │ │ │ │ │ │ │ │ │ │ │
+// │ 1 │ │ 1 │ │ 1 │ │ 3 │ │ │ │
+// └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ │ │ │
+// │ │ │ │ 5 Sec │ │
+// ┌────┴───┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ │ │ │
+// │Blocking│ │ T │ │ T │ │ T │ │ │ │
+// └────┬───┘ │ a │ │ a │ │ a │ │ │ │
+// │ │ s │ │ s │ │ s │ │ 8 Sec │
+// ┌─┴─┐ │ k │ │ k │ │ k │ │ │ │
+// │ T │ │ │ │ │ │ │ │ │ │
+// │ a │ │ 2 │ │ 2 │ │ 4 │ │ │ │
+// │ s │ └─┬─┘ ├───┤ ├───┤ │ │ │
+// │ k │ │ │┼┼┼│ │┼┼┼│ ▼ │ 10 Sec
+// │ │ ┌─┴─┐ └───┴────────┴───┴───────── │ │
+// │ 1 │ │ T │ │ │
+// └─┬─┘ │ a │ │ │
+// │ │ s │ │ │
+// ┌─┴─┐ │ k │ │ │
+// │ T │ │ │ │ │
+// │ a │ │ 1 │ │ │
+// │ s │ ├───┤ │ │
+// │ k │ │┼┼┼│ ▼ │
+// │ │ └───┴──────────────────────────────────────────── │
+// │ 2 │ │
+// ├───┤ │
+// │┼┼┼│ ▼
+// └───┴────────────────────────────────────────────────────────────────
+//
+//
+// The diagram was modeled on the one in a blog in which the differences
+// between asynchronous processing and multithreading are explained in detail:
+// https://blog.devgenius.io/multi-threading-vs-asynchronous-programming-what-is-the-difference-3ebfe1179a5
+//
+// Our exercise is essentially about clarifying the approach in Zig and
+// therefore we try to keep it as simple as possible.
+// Multithreading in itself is already difficult enough. ;-)
+//
+const std = @import("std");
+
+pub fn main() !void {
+ // This is where the preparatory work takes place
+ // before the parallel processing begins.
+ std.debug.print("Starting work...\n", .{});
+
+ // These curly brackets are very important, they are necessary
+ // to enclose the area where the threads are called.
+ // Without these brackets, the program would not wait for the
+ // end of the threads and they would continue to run beyond the
+ // end of the program.
+ {
+ // Now we start the first thread, with the number as parameter
+ const handle = try std.Thread.spawn(.{}, thread_function, .{1});
+
+ // Waits for the thread to complete,
+ // then deallocates any resources created on `spawn()`.
+ defer handle.join();
+
+ // Second thread
+ const handle2 = try std.Thread.spawn(.{}, thread_function, .{-4}); // that can't be right?
+ defer handle2.join();
+
+ // Third thread
+ const handle3 = try std.Thread.spawn(.{}, thread_function, .{3});
+ defer ??? // <-- something is missing
+
+ // After the threads have been started,
+ // 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.fromMilliseconds(400), .awake);
+ std.debug.print("Some weird stuff, after starting the threads.\n", .{});
+ }
+ // After we have left the closed area, we wait until
+ // the threads have run through, if this has not yet been the case.
+ std.debug.print("Zig is cool!\n", .{});
+}
+
+// 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(id: usize) !void {
+ var io_instance: std.Io.Threaded = .init_single_threaded;
+ const io = io_instance.io();
+ 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 = 300 * ((5 - id % 3) - 2);
+ try io.sleep(std.Io.Duration.fromMilliseconds(@intCast(work_time)), .awake);
+
+ 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,
+// e.g. by setting up a pool and allowing the threads to communicate
+// with each other using semaphores.
+//
+// But that's a topic for another exercise.