fork join vs join_any vs join_none in SystemVerilog (With Timing Diagram)
In short: fork starts several threads at the same time. The keyword that closes it decides when the parent moves on. join waits until all threads finish. join_any waits until the first one finishes, and the others keep running. join_none doesn’t wait at all; the threads start as soon as the parent pauses. Use wait fork to wait for running children later, and disable fork to stop them.
A testbench does many things at once.
Drive stimulus. Watch the bus. Check responses. Run a timeout.
fork is how you start those things in parallel. The only real question is: when does the code after the fork run?
Think of it like a restaurant kitchen
The head chef (the parent thread) hands three orders to three cooks (the child threads).
- **
join: the chef waits until all three** dishes are ready, then carries them out together. A table that ordered together is served together. - **
join_any: the chef carries out the first** dish that’s ready. The other cooks keep cooking. - **
join_none:** the chef hands over the orders and goes straight back to the front to take the next table. The cooks start the moment the chef walks away.
And two more tools:
- **
wait fork:** the chef stands at the pass and waits for every cook still working. - **
disable fork:** the chef shouts “stop, cancel those orders!” to the cooks still working.
The three forms in code
Three workers take 10, 20 and 30 ns. Watch when the parent continues.
// Three threads that take 10, 20 and 30 ns. When does the parent move on?
module tb;
task automatic worker(string name, int delay);
#delay $display("%0t ns: %s done", $time, name);
endtask
initial begin
$display("--- fork...join");
fork
worker("A", 10);
worker("B", 20);
worker("C", 30);
join
$display("%0t ns: parent continues", $time);
$display("--- fork...join_any");
fork
worker("A", 10);
worker("B", 20);
worker("C", 30);
join_any
$display("%0t ns: parent continues", $time);
wait fork; // let B and C finish before the next demo
$display("--- fork...join_none");
fork
worker("A", 10);
worker("B", 20);
worker("C", 30);
join_none
$display("%0t ns: parent continues", $time);
wait fork;
$finish;
end
endmodule
Output (tested on Verilator 5.020, 11 Nov 2026):
--- fork...join
10 ns: A done
20 ns: B done
30 ns: C done
30 ns: parent continues
--- fork...join_any
40 ns: A done
40 ns: parent continues
50 ns: B done
60 ns: C done
--- fork...join_none
60 ns: parent continues
70 ns: A done
80 ns: B done
90 ns: C done
Read each block against its start time:
- join started at 0. The parent continued at 30 ns, when the slowest worker finished.
- join_any started at 30. The parent continued at 40 ns, when A finished. B and C kept going to 50 and 60.
- join_none started at 60. The parent continued at 60 ns, right away. The workers finished later.
fork join vs join_any vs join_none
fork...join | fork...join_any | fork...join_none | |
|---|---|---|---|
| Parent continues when | All threads finish | The first thread finishes | Immediately |
| Other threads after the parent moves on | None left | Keep running | All keep running |
| In the run above (10/20/30 ns workers) | After 30 ns | After 10 ns | After 0 ns |
| Kitchen picture | Serve the whole table together | Serve the first dish | Hand over orders, go back to the front |
| Typical use | Drive and monitor, then check | Response vs timeout | Start background monitors, spawn N parallel jobs |
| Allowed in a function? | No | No | Yes (it doesn’t block) |
This is IEEE 1800-2017, section 9.3.2.
The loop-variable trap with join_none
This is the most-asked fork question on the forums. You fork one thread per loop pass, and every thread sees the last value of the loop variable.
Why? With join_none, the children don’t start until the parent pauses. By then the loop has finished, and i already holds its final value. Every child reads the same i (IEEE 1800-2017, 9.3.2).
In kitchen terms: the chef writes “table number” on a single whiteboard and changes it for each order. The cooks only look at the board after the chef has left, and they all read the last number.

// The loop-variable trap: every forked thread sees the final value of i.
module tb;
initial begin
$display("--- wrong");
for (int i = 0; i < 3; i++)
fork
#1 $display("thread sees i = %0d", i);
join_none
wait fork;
$display("--- fixed");
for (int i = 0; i < 3; i++) begin
automatic int k = i; // a fresh copy for each loop pass
fork
#1 $display("thread sees k = %0d", k);
join_none
end
wait fork;
$finish;
end
endmodule
What you’ll see:
- Wrong version: per the standard, all three threads print
i = 3. Forum users reported exactly this (every thread got 4 with a 5-pass loop, or 3 with a 3-pass loop). Verilator 5.020 happens to print 0, 1, 2 here, so don’t use it to learn this rule. - Fixed version (tested on Verilator 5.020):
thread sees k = 0
thread sees k = 1
thread sees k = 2
The fix is to give each pass its own whiteboard: declare an automatic copy inside the loop body, before the fork.
wait fork and disable fork
- **
wait forkwaits for every direct child** the current thread has started, including ones from earlier forks you may have forgotten about. - **
disable fork** kills every child the current thread has started, and their children too (IEEE 1800-2017, 9.6.1 and 9.6.3).
Both act on the whole family of the current thread, not just the last fork. That’s why the safe pattern wraps them in their own little thread:
fork
begin : guard // a new parent with only these children
fork
...
join_any
disable fork; // only touches the children of 'guard'
end
join
Think of it as giving the chef a separate small kitchen for this job. Shouting “stop!” there doesn’t cancel orders in the main kitchen.

Working example 1: wait for a response, with a timeout
Every AXI or APB driver needs this: wait for the response, but don’t hang forever if the slave is stuck.
// Wait for a response, but give up after a timeout. join_any + disable fork.
module tb;
bit resp_seen;
task automatic wait_for_response(int resp_delay, int timeout);
resp_seen = 0;
fork
begin : guard // keeps disable fork away from other threads
fork
begin #resp_delay resp_seen = 1; end // stands in for "wait for BVALID"
#timeout; // watchdog
join_any
disable fork; // kill whichever one is still running
end
join
if (resp_seen) $display("%0t ns: response arrived", $time);
else $display("%0t ns: TIMEOUT after %0d ns", $time, timeout);
endtask
initial begin
wait_for_response(15, 50); // fast slave
wait_for_response(80, 50); // stuck slave
$finish;
end
endmodule
Output (Verilator 5.020):
15 ns: response arrived
65 ns: TIMEOUT after 50 ns
- The fast slave answered at 15 ns.
join_anyreleased the parent, anddisable forkkilled the 50 ns watchdog. - The stuck slave would have taken 80 ns. The watchdog won at 15 + 50 = 65 ns, and
disable forkkilled the response wait.
Working example 2: continue after any 3 of 5 jobs
Sometimes you need “wait for some, not all”. A semaphore counts the finished jobs.
// Start 5 jobs, continue as soon as any 3 finish, then stop the other 2.
module tb;
semaphore done = new(0); // starts with no keys
task automatic job(int id, int delay);
#delay $display("%0t ns: job %0d finished", $time, id);
done.put(1); // hand in one key
endtask
initial begin
fork
begin : guard
fork
job(1, 10); job(2, 15); job(3, 5); job(4, 20); job(5, 25);
join_none
done.get(3); // blocks until 3 keys are in
$display("%0t ns: 3 jobs done, stopping the rest", $time);
disable fork; // kills jobs 4 and 5
end
join
#50 $display("%0t ns: end of test (jobs 4 and 5 never printed)", $time);
$finish;
end
endmodule
Output (Verilator 5.020):
5 ns: job 3 finished
10 ns: job 1 finished
15 ns: job 2 finished
15 ns: 3 jobs done, stopping the rest
65 ns: end of test (jobs 4 and 5 never printed)
Jobs 4 and 5 would have finished at 20 and 25 ns, but they never print. disable fork stopped them at 15 ns.
Common mistakes (and the fix for each)
These come from real Verification Academy threads, most answered by Dave Rich of Siemens. Each is retold in my own words with a link.
1. Using the loop variable inside fork…join_none
- What happened: in 2013 an engineer forked
thread(i)in a 5-pass loop, and every thread got 4 (Verification Academy, 2013). The same question came back in 2021, with every thread printing 3 (Verification Academy, 2021). - Why: the children start only after the parent pauses, when the loop has already finished.
- Fix:
automatic int k = i;in the loop body, before the fork, and usekin the thread. Declare and set it in one statement; a separate assignment inside the fork can race.
2. disable fork stopping more than you meant
- What happened: in 2017 someone used
disable forkinside a UVM sequence and asked how far it reaches (Verification Academy, 2017). - Why: it kills every child of the thread that runs it, from all earlier forks, not just the last one.
- Fix: wrap the fork in
fork begin ... end joinsodisable forkonly sees the children you mean. Or keepprocesshandles fromprocess::self()and callkill()on just the ones you want.
3. wait fork waiting for a thread you forgot about
- What happened: in 2017 an engineer was unsure what
wait forkwaits for, and a reply got it wrong (Verification Academy, 2017). - Why: it waits for every direct child still running, including ones started by earlier forks in the same thread. A forgotten background monitor means
wait forknever returns. - Fix: use the same
begin ... endguard around the forks you want to wait for.
4. Thinking join_any stops the other threads
- What happened: in 2018 someone compared
fork...joinwithfork...join_anypluswait forkand asked if they were the same (Verification Academy, 2018). - Why:
join_anyonly releases the parent early. The other threads keep running, which our output shows (B and C finishing at 50 and 60 ns). - Fix: if you want them gone, follow
join_anywithdisable fork, inside a guard.
5. Counting finished threads with a shared counter
- What happened: in 2020 an engineer ran 5 tasks, bumped a shared
countin each, waited forcount == 3, then disabled the rest. It worked, but only by luck (Verification Academy, 2020). - Why: if two tasks finish in the same time step,
countcan jump from 2 to 4 andwait(count == 3)never fires. - Fix: use a
semaphore, exactly like working example 2: each job puts a key, the parent gets 3.
Frequently asked questions
What is the difference between join, join_any and join_none?
join waits for all threads, join_any waits for the first one, and join_none doesn’t wait.
Does join_any stop the other threads?
No. They keep running. Add disable fork (inside a guard) to stop them.
When do join_none threads actually start?
When the parent pauses, for example at its next #delay, @ or wait. Not at the join_none line itself.
What does wait fork wait for?
All direct children of the current thread that are still running, from every fork it has done.
What does disable fork kill?
All children of the current thread, and their children. Wrap it in fork begin ... end join to limit it.
Can I use fork inside a function?
Only fork...join_none, because a function can’t wait.
How do I stop just one thread?
Save its handle with process p = process::self(); inside the thread, then call p.kill().
What to read next
- Semaphore in SystemVerilog: the key counter behind example 2
- Mailbox in SystemVerilog: passing data between forked threads
- @event vs wait(event.triggered): signalling between threads
References
- IEEE 1800-2017, SystemVerilog standard: section 9.3.2 (parallel blocks: join, join_any, join_none), 9.6.1 (wait fork), 9.6.3 (disable fork), 9.7 (fine-grain process control), 15.3 (semaphores).
- Verification Academy forum threads linked in the mistakes section (accessed 11 Nov 2026).
- Examples 1, 3 and 4 tested on Verilator 5.020 with
--timingon 11 Nov 2026. In the loop example, only the fixed version’s output is shown.
Part of the learning path SystemVerilog for verification

Awesome Hardik….Good job done…..well explained โฆ..keep up the good work
Thank you Soumya ๐ Really Appreciate it ๐
Excellent Hardik!!
Thanks, Debojyoti ๐
Very good explanation Hardik !
Thanks, Amulya ๐
Very nice explanation Hardik!! Thank you!!
Thank you Kalpana ๐