mirror of
https://codeberg.org/ziglings/exercises.git
synced 2026-06-10 09:00:00 +00:00
Insert space for additional async exercises
This commit is contained in:
129
exercises/107_threading.zig
Normal file
129
exercises/107_threading.zig
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user