SystemVerilog 线程同步:fork、event、semaphore 与 mailbox
更新时间:2026-10-01。driver、monitor、generator 各自是并发线程,还常要等中断、等响应。这一篇讲怎么开线程、怎么安全同步。
本文要回答的问题
- 三种 join(join/join_any/join_none)行为有何不同?
- event 为什么会出现“触发了却没等到”的时序坑?
- 多个线程抢同一资源、在线程间传事务分别用什么?
- 怎么给等待加超时、怎么干净地停掉线程?
一、fork 的三种汇合方式
systemverilog
fork
task_a();
task_b();
join // 等两个都完成
join_any // 任一完成就往下(另一个仍在跑)
join_none // 立即往下,线程后台运行join_any 后通常要 disable fork 清理未完成分支,否则孤儿线程残留;join_none 开的后台线程在父进程结束时要用 wait fork 收敛。
二、event:一次性通知
systemverilog
event e;
fork
begin wait(e.triggered); $display("got it"); end
begin #10 -> e; end
join-> e触发,wait(e.triggered)在当前时隙内都能捕获,比@e更抗“零时刻竞争”。- 经典坑:触发发生在等待进程还没挂起之前,用
@e会永远错过;对时序不确定的握手,用triggered或命名事件配合握手协议。
三、semaphore:资源许可
systemverilog
semaphore bus_key;
bus_key = new(1); // 1 把钥匙 → 互斥
task access();
bus_key.get(); // 拿不到就阻塞
// ...临界区,只有一个线程进入...
bus_key.put();
endtasknew(n) 给 n 个许可,模拟“n 个相同资源”。try_get() 非阻塞版本。适合多 master 抢总线、限制并发访问。
四、mailbox:线程间传对象
systemverilog
mailbox #(transaction) mbx = new();
// generator
mbx.put(tr); // 满则阻塞
// driver
forever begin mbx.get(tr); drive(tr); end泛型 mailbox 限定类型更安全;put/get 阻塞,try_put/try_get/peek 非阻塞或只看不取。generator→driver、monitor→scoreboard 都靠它。需要“容量限制做背压”时 new(depth)。
五、同步原语选型

六、超时与停止
systemverilog
fork
begin wait(irq == 1); end
begin #1000; $error("timeout"); end
join_any
disable fork;“等待 + 看门狗”用 fork/join_any/disable fork 是标准写法,避免平台在 DUT 卡死时永远挂住。
七、常见坑
- join_any 后不 disable fork,孤儿线程改信号、重复触发。
- 用
@e等待先于触发的事件,永久挂起。 - mailbox 不限定类型,put 进错误对象,get 后类型不匹配。
- semaphore get 后异常退出没 put,钥匙泄漏导致死锁。
- 多个后台线程随仿真结束未收敛,结果不确定。
小结
总结
SystemVerilog 线程用 fork...join/join_any/join_none 开并发(join_any 后要 disable fork、join_none 后台线程用 wait fork 收敛);三种同步原语各司其职——event 做一次性通知(用 wait(e.triggered) 而非 @e 避免零时刻竞争漏触发)、semaphore 用 N 把钥匙做资源互斥、泛型 mailbox 在线程间传事务对象(put/get 阻塞、try_* /peek 非阻塞、new(depth) 做背压),再加"等待 + 看门狗 + join_any/disable fork"防止 DUT 卡死时平台永久挂起。