Day 78: std.synchronization模块:高级同步原语
1. 引言:超越互斥锁
Section titled “1. 引言:超越互斥锁”我们在std.thread中已经学习了互斥锁(Mutex),它用于保护共享数据,确保一次只有一个线程可以访问。然而,并发编程中的协调问题远比这更复杂。有时我们需要:
- 允许多个(但有数量限制的)线程同时访问一个资源池。
- 让多个线程一直等待,直到所有线程都到达了某个同步点。
std.sync(或std.synchronization)模块提供了更高级的同步原语来解决这些问题,主要是信号量(Semaphore)和屏障(Barrier)。
2. Semaphore:资源计数
Section titled “2. Semaphore:资源计数”信号量可以看作是一个管理着一组“许可证”的计数器。一个线程要想执行某个操作,必须先从信号量那里获取一个许可证(wait())。如果许可证已经发完,线程就会阻塞,直到有其他线程释放一个许可证(post())。
这对于控制对有限资源的访问非常有用,例如数据库连接池或线程池中的工作任务。
init(initial_permits: usize): 初始化一个信号量,并设定初始许可证数量。wait(): 等待并获取一个许可证,使计数器减一。post(): 释放一个许可证,使计数器加一。
const std = @import("std");
var sem = std.sync.Semaphore.init(2); // 最多允许2个并发访问
fn worker(id: u32) void { std.debug.print("Worker {d} waiting...\n", .{id}); sem.wait(); std.debug.print("Worker {d} acquired semaphore!\n", .{id}); std.time.sleep(1 * std.time.ns_per_s); sem.post(); std.debug.print("Worker {d} released semaphore.\n", .{id});}如果同时启动3个worker,你会发现总是有两个先进入,第三个必须等待其中一个完成后才能进入。
3. Barrier:同步点
Section titled “3. Barrier:同步点”屏障(Barrier)用于让一组线程相互等待,直到所有线程都到达了同一个执行点。想象一下赛跑,所有选手都必须在起跑线上准备好,然后才能同时出发。
init(count: usize): 初始化一个屏障,并指定需要等待的线程数量。wait(): 通知屏障“我已到达”,然后阻塞,直到所有count个线程都调用了wait()。
const std = @import("std");
const num_threads = 4;var barrier = std.sync.Barrier.init(num_threads);
fn task(id: u32) void { std.debug.print("Task {d} doing pre-barrier work...\n", .{id}); // ...
// 等待所有其他任务到达屏障 barrier.wait();
// 这部分代码只有在所有4个任务都调用了barrier.wait()之后才会执行 std.debug.print("Task {d} doing post-barrier work...\n", .{id});}4. 示例:简单的线程池
Section titled “4. 示例:简单的线程池”信号量可以用来实现一个简单的固定大小的线程池的任务队列。
const std = @import("std");
var task_queue = std.ArrayList(fn()void).init(std.testing.allocator);var tasks_available = std.sync.Semaphore.init(0);
fn worker_thread() void { while (true) { tasks_available.wait(); // 等待有任务可用 const task = task_queue.pop(); task(); }}
fn submit_task(task: fn()void) !void { try task_queue.append(task); tasks_available.post(); // 通知worker有新任务}5. 实践练习:实现一个读写锁
Section titled “5. 实践练习:实现一个读写锁”使用std.thread.Mutex和一个std.sync.Semaphore来构建一个读写锁(Readers-Writer Lock)。这个锁应该允许多个“读者”同时访问,但“写者”必须独占访问。提示:你需要一个互斥锁来保护读者计数器,并用信号量来控制写者的独占访问。
6. 常见问题
Section titled “6. 常见问题”-
自旋锁(Spinlock) vs. 阻塞锁
Mutex,Semaphore和Barrier都是阻塞式的。当一个线程等待时,它会被操作系统挂起,不消耗CPU。自旋锁则是在一个循环中不断检查锁是否可用,这会消耗CPU,但避免了线程上下文切换的开销。自旋锁只适用于锁的持有时间极短的场景。 -
条件变量(Condition Variables) 条件变量是另一种重要的同步原语,它允许线程等待某个特定条件变为真。
std.thread.Cond提供了这个功能。它总是与一个Mutex配合使用。 -
如何避免死锁? 死锁是并发编程中的经典问题。一个常见的死锁场景是:线程A持有锁1并等待锁2,而线程B持有锁2并等待锁1。预防死锁的一个基本规则是:所有线程都必须以相同的顺序获取锁。
7. 总结:futex的力量
Section titled “7. 总结:futex的力量”Zig的同步原语在底层通常是基于操作系统的futex(Fast Userspace Mutex)或类似的机制。这意味着在没有竞争的情况下,加锁和解锁操作可以在用户空间完成,非常快。只有当发生竞争(即一个线程需要等待)时,才会陷入内核,进行线程调度。
通过掌握Semaphore和Barrier等高级同步工具,你能够构建出更复杂、更精细、性能更高的并发应用程序,解决那些仅靠互斥锁难以应对的协调问题。