A fork spawns one child per statement. join resumes the parent after all children end, join_any after the first, and join_none at once.
Children of join_none do not start until the parent next blocks. That deferral is the root of the loop bug in the next section.
Ending | Parent resumes | Children after resume |
|---|---|---|
| After all end | None left |
| After the first ends | Rest keep running |
| At once | All run in background |
Pair each ending with its job: join for lockstep phases, join_any for racing work against a timeout, join_none for daemon monitors that never return.
Spawning one child per loop iteration hits the famous capture bug. The children start after the loop ends, so every child reads the shared loop variable at its final value.
The cure is an automatic copy inside the fork, evaluated at spawn time in the parent context. Each child then owns a private value frozen at the right iteration.
The full bench sits under Testbench below. A copy outside the fork still shares one variable across children in a static context.
disable fork kills every live child of the calling process, including earlier join_none daemons. That blast radius is the usual cause of monitors that silently stop mid-test.
The isolation idiom shrinks it: wrap the racing pair in fork begin ... end join, so the disable only reaches the pair. wait fork is the matching cleanup barrier before final checks.
// snippet
// Isolation wrapper: the disable below cannot reach the daemon monitor.
fork mon_run(); join_none // daemon, must survive
fork
begin
fork
main_sequence();
#1000;
join_any
disable fork; // kills only the racing pair
end
joinThe process class gives a handle to a running thread, so one thread can check on or stop another:
timeunit 1ns; // one time unit for the classes and the module
timeprecision 1ps;
class worker_pool_c;
process workers[3];
task start();
foreach (workers[i]) begin
automatic int idx = i;
fork
begin
workers[idx] = process::self();
forever begin
#(10 * (idx + 1));
$display("[%0t] worker %0d tick", $time, idx);
end
end
join_none
end
endtask
task stop_one(int idx);
if (workers[idx] != null && workers[idx].status() != process::FINISHED)
workers[idx].kill();
endtask
endclass
module top_proc;
worker_pool_c pool = new();
initial begin
pool.start();
#100;
pool.stop_one(1);
#100;
if (pool.workers[1].status() != process::KILLED)
$fatal(1, "FAIL: worker 1 was not killed");
if (pool.workers[0].status() == process::KILLED || pool.workers[2].status() == process::KILLED)
$fatal(1, "FAIL: stop_one(1) killed another worker");
$display("PASS: stop_one(1) killed worker 1 and left workers 0 and 2 running");
$finish;
end
endmodule[10000] worker 0 tick
[20000] worker 1 tick
[20000] worker 0 tick
[30000] worker 2 tick
[30000] worker 0 tick
[40000] worker 1 tick
[40000] worker 0 tick
[50000] worker 0 tick
[60000] worker 2 tick
[60000] worker 1 tick
... 9 more lines ...
[120000] worker 0 tick
[130000] worker 0 tick
[140000] worker 0 tick
[150000] worker 2 tick
[150000] worker 0 tick
[160000] worker 0 tick
[170000] worker 0 tick
[180000] worker 2 tick
[180000] worker 0 tick
[190000] worker 0 tick
PASS: stop_one(1) killed worker 1 and left workers 0 and 2 running
[200000] worker 0 tickAll three print the final value. The children start after the loop ends and share one variable holding that value.
join_any plus bare disable fork in a task that also spawned a monitor. What breaks?The monitor dies too, since it is a child of the same task. Wrap the race in an inner fork block to isolate the disable.
join_none, each with automatic int k = i. What do they write into seen[0..2]?0, 1 and 2. The automatic copy is made when each child is spawned, so each child keeps its own index even though the children run after the loop ends.
Three children, three private copies, one ordered check.
module tb_fork_loop;
timeunit 1ns;
timeprecision 1ps;
int seen[3];
int errors = 0;
initial begin
for (int j = 0; j < 3; j++) seen[j] = -1;
for (int i = 0; i < 3; i++) begin
fork
automatic int k = i;
begin
#1;
seen[k] = k;
end
join_none
end
#10;
for (int j = 0; j < 3; j++) if (seen[j] !== j) errors++;
if (errors == 0) $display("PASS: 3 forked children each kept its own index");
else $fatal(1, "FAIL: seen=%0d,%0d,%0d", seen[0], seen[1], seen[2]);
$finish;
end
endmodulePASS: 3 forked children each kept its own indexRun with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.