Ziglings 笔记 104: 多线程 (Multithreading)
火力全开:多线程
在 Ziglings 的第 104 个练习中,我们终于触及了榨干 CPU 性能的关键技术——多线程 (Multithreading)。
与 async 这种轻量级的协作式多任务不同,std.Thread 对应的是操作系统级的内核线程。这意味着你的代码可以真正地同时跑在多个 CPU 核心上,实现物理上的并行。
挑战:三个火枪手
我们需要启动三个线程,让它们并行执行 thread_function。
主线程需要等待它们全部完成后才能退出,否则它们可能会被腰斩。
解决方案
我们需要正确地启动线程,并为每个线程注册清理操作(join)。
const std = @import("std");
pub fn main() !void {
std.debug.print("Starting work...\n", .{});
// 创建一个作用域,用于管理线程生命周期
{
// 1. 启动线程 1
// 参数传递方式:.{1} (作为元组)
const handle1 = try std.Thread.spawn(.{}, thread_function, .{1});
// 确保最终等待线程结束并释放资源
defer handle1.join();
// 2. 启动线程 2
// 修复点:确保参数正确传入
const handle2 = try std.Thread.spawn(.{}, thread_function, .{2});
defer handle2.join();
// 3. 启动线程 3
const handle3 = try std.Thread.spawn(.{}, thread_function, .{3});
// 修复点:别忘了 join 第三个线程!
// 如果不 join,主线程结束时该线程可能还在运行,导致未定义行为或崩溃。
defer handle3.join();
// 在主线程中做点别的事...
// 此时,thread 1, 2, 3 和主线程正在同时运行(如果 CPU 核心够多)
std.time.sleep(1 * std.time.ns_per_s);
std.debug.print("Main thread working...\n", .{});
}
std.debug.print("All work done! Zig is cool!\n", .{});
}
fn thread_function(num: usize) !void {
std.debug.print("thread {d}: started.\n", .{num});
// 模拟耗时工作
// 注意:std.time.sleep 接收纳秒
std.time.sleep(1 * std.time.ns_per_s);
std.debug.print("thread {d}: finished.\n", .{num});
}
核心知识点总结
1. spawn 与 join
spawn: 向操作系统申请创建一个新线程。这涉及到系统调用,有一定的开销。join: 阻塞当前线程,直到目标线程退出。它同时负责清理线程栈等资源。如果你不关心线程何时结束,可以使用detach(),但那样你就失去了对它的控制。
2. 这里的 defer 陷阱
在这个简单的例子中,我们对每个线程立即调用了 defer join。
由于 defer 是后进先出(LIFO)执行的,且是在作用域结束时执行。
这意味着主线程会在 } 处依次等待 handle3, handle2, handle1。虽然等待是串行的,但线程本身的执行是并行的。
3. 数据竞争 (Data Race)
虽然本例没有涉及共享数据,但在多线程编程中,多个线程同时修改同一个变量是非常危险的。Zig 提供了 std.Thread.Mutex(互斥锁)等工具来保护临界区。在后续的进阶学习中,这是必须掌握的内容。
后续预告:我们已经学会了如何创建线程。但是,如果线程之间需要说话怎么办?比如线程 A 生产数据,线程 B 处理数据?下一篇,我们将可能会接触 Atomic (原子操作) 或者 Channel (通道) 的概念(如果 Ziglings 包含的话),或者回归标准库的其他实用功能。