Flavours of Fork..Join
In short: A fork...join block runs its statements in parallel. The three flavours differ in when the parent thread continues: join waits for all spawned threads, join_any waits for the first one to finish, and join_none waits for none and moves on at once. Two helpers go with them: wait fork blocks until all child threads complete, and disable fork kills the still-running children.
SystemVerilog uses fork...join to start several threads that run at the same time. This is the main way testbenches model things that happen in parallel, like a driver, a monitor, and a clock all working together. The one choice you make is how long the code after the fork should wait for those threads. That choice is the flavour: join, join_any, or join_none.
A simple way to picture it
Imagine you send three friends to run errands. With join, you wait at the door until all three are back before you leave. With join_any, you leave as soon as the first one returns, while the others keep going. With join_none, you send them off and immediately leave yourself; they do their errands on their own. wait fork is you deciding later to pause until everyone is finally back, and disable fork is you calling everyone to stop right now.
The three flavours at a glance
| Construct | Parent continues when… | Other threads |
|---|---|---|
fork...join | All spawned threads have finished | n/a, all are done |
fork...join_any | Any one spawned thread finishes | Keep running in the background |
fork...join_none | Immediately, without waiting | Keep running in the background |
fork…join: wait for everyone
The parent thread blocks until every thread inside the fork has finished. The threads themselves run in parallel, so their order depends on their delays, not on the order they are written.
module fork_join_ex;
initial begin
$display("[%0t] before fork", $time);
fork
#15 $display("[%0t] thread A", $time);
#5 $display("[%0t] thread B", $time);
#10 $display("[%0t] thread C", $time);
join
$display("[%0t] after join", $time); // runs at time 15
end
endmoduleThread B prints first at time 5, then C at 10, then A at 15. The “after join” line waits for the slowest thread, so it prints at time 15.
fork…join_any: wait for the first
The parent continues as soon as any one thread finishes. The rest are not killed; they keep running in the background.
module fork_join_any_ex;
initial begin
$display("[%0t] before fork", $time);
fork
#15 $display("[%0t] thread A", $time);
#5 $display("[%0t] thread B", $time);
#10 $display("[%0t] thread C", $time);
join_any
$display("[%0t] after join_any", $time); // runs at time 5
end
endmoduleThread B finishes first at time 5, so “after join_any” prints at time 5. Threads A and C still print later, at 10 and 15, because they were not stopped.
fork…join_none: do not wait
The parent does not wait at all. It spawns the threads and moves straight to the next line. The threads run when the parent later hits a blocking statement or finishes.
module fork_join_none_ex;
initial begin
$display("[%0t] before fork", $time);
fork
#15 $display("[%0t] thread A", $time);
#5 $display("[%0t] thread B", $time);
join_none
$display("[%0t] after join_none", $time); // runs at time 0
#20; // now the spawned threads get a chance to run
end
endmodule“after join_none” prints immediately at time 0. The spawned threads then print at 5 and 15 while the parent waits on the #20 delay.
wait fork: pause until all children finish
After a join_any or join_none, some threads may still be running. wait fork makes the parent pause until every child thread it started has finished. This stops the simulation ending too early.
module wait_fork_ex;
initial begin
fork
begin #15 $display("[%0t] thread A done", $time); end
begin #30 $display("[%0t] thread B done", $time); end
join_any // continues at time 15
wait fork; // now pause until B also finishes (time 30)
$display("[%0t] all children finished", $time);
end
endmoduleWithout wait fork, the code after join_any would run at time 15 and could end the run while thread B is still going. With it, the parent waits until thread B finishes at time 30.
disable fork: stop the remaining children
Sometimes you want the opposite: once one thread wins, kill the rest. disable fork terminates all active threads spawned by the current fork. This is the pattern used in interrupt watchers and timeout checks.
module disable_fork_ex;
initial begin
fork
begin #15 $display("[%0t] real work done", $time); end
begin #100 $display("[%0t] TIMEOUT", $time); end
join_any // whichever finishes first
disable fork; // kill the loser (here, the timeout)
$display("[%0t] moving on", $time);
end
endmoduleThe real work finishes at time 15, so join_any unblocks and disable fork kills the pending timeout thread before it can fire. This “work vs timeout” race is one of the most useful fork patterns.
Disabling one named thread
To stop just one specific thread rather than all of them, give the thread a label and disable that label by name.
module disable_named_ex;
initial begin
fork
begin : slow_thread
#30 $display("[%0t] slow finished", $time);
end
begin : fast_thread
#15 $display("[%0t] fast finished", $time);
end
join_any
disable slow_thread; // stop only this one
$display("[%0t] kept fast, stopped slow", $time);
end
endmoduleExpected behaviour, in plain words
Across these examples the pattern is consistent: threads print in order of their delays, and the line after the fork prints at time 15 for join, at time 5 for join_any, and at time 0 for join_none. In the timeout example, the real work at time 15 wins and the timeout is killed, so you never see “TIMEOUT”. I have reviewed this against the language rules rather than run it, so check the exact timestamps in your own simulator or on EDA Playground.
When to use each
- fork…join when every parallel task must complete before you continue, such as setting up several interfaces at once.
- fork…join_any for a race, such as “finish when the response arrives or the timeout expires”.
- fork…join_none to launch background monitors or checkers that should run for the rest of the test.
- wait fork to make sure no background thread is left unfinished before you end a phase or test.
- disable fork to cancel the losers after a race, so a timeout thread does not linger and fire later.
Common mistakes to avoid
- Thinking join_any kills the others. It does not. The remaining threads keep running unless you call disable fork.
- Ending the test after join_any or join_none. Background threads may still be active; use wait fork to let them finish or disable fork to stop them.
- Forgetting each begin…end is one thread. Statements inside a begin…end run in sequence; only separate begin…end blocks run in parallel.
- Trying to disable one thread without a label. disable needs a named begin…end to target a single thread.
- Sharing variables across threads without care. Use automatic variables in loops that fork, or all threads may capture the same final value.
Keep learning
Fork and join are the base of parallel stimulus. See them in action in handling interrupts in UVM, which uses a two-level fork with disable fork. For where threads live in a testbench, read the UVM testbench architecture, and see class basics in encapsulation in SystemVerilog. More is in our SystemVerilog guides and interview questions.
Frequently asked questions
What is the difference between join, join_any, and join_none?
With join the parent thread waits for all forked threads to finish. With join_any it continues as soon as the first thread finishes. With join_none it continues immediately without waiting for any thread. In join_any and join_none the other threads keep running in the background.
Does join_any stop the other threads?
No. join_any only unblocks the parent when the first thread finishes. The remaining threads continue to run. If you want to stop them, call disable fork after the join_any.
What does wait fork do?
wait fork makes the current thread pause until all the child threads it spawned have finished. It is often used after join_any or join_none to make sure no background thread is left running before the test or phase ends.
What does disable fork do?
disable fork terminates all active threads that were spawned by the current fork block. It is commonly used after a race, for example to kill a timeout thread once the real work finishes first.
How do I stop only one thread inside a fork?
Give that thread a label using begin : name … end, then call disable name. This terminates just the named thread and leaves the others running.
Why do all my forked loop iterations see the same value?
Because they share the loop variable. Declare an automatic variable inside the loop and copy the index into it before forking, so each thread captures its own value rather than the final one.



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 π