Plain fork...join waits until every one of its spawned threads finishes — which means the simulation hangs if even one of those threads runs forever and never completes. fork...join_any is the variation that avoids this: the main thread resumes as soon as any one of the forked threads finishes, not all of them.
Fork off five threads with join_any, and the main thread resumes as soon as any single one of those five finishes — the remaining four keep running in the background after that.
7 min read | Intermediate Level
What You'll Learn
- Why plain
fork...joincan hang a simulation, and howjoin_anyavoids that - How
join_anydecides exactly when the parent thread resumes - How a nested
fork...join_anybehaves, and what "finishing" means for the nested block itself - What happens to the remaining threads that are still running once the parent resumes
Syntax
fork
// Thread 1
// Thread 2
// ...
// Thread N
join_any
A Basic fork...join_any Example
module tb;
initial begin
$display ("[%0t] Main Thread: Fork join going to start", $time);
fork
print (20, "Thread1_0");
print (30, "Thread1_1");
print (10, "Thread2");
join_any
$display ("[%0t] Main Thread: Fork join has finished", $time);
end
// Note that this task needs to be automatic
task automatic print (int _time, string t_name);
#(_time) $display ("[%0t] %s", $time, t_name);
endtask
endmodule
Output
ncsim> run [0] Main Thread: Fork join going to start [10] Thread2 [10] Main Thread: Fork join has finished [20] Thread1_0 [30] Thread1_1 ncsim: *W,RNQUIE: Simulation is complete.
All three threads are forked at 0ns. Thread2 finishes first, at 10ns, and since that's the very first thread to complete, the main thread resumes right there at 10ns — without waiting for Thread1_0 (20ns) or Thread1_1 (30ns) at all. Both of those keep running independently in the background and print later, on their own schedule.
Quick Check
Q: If Thread2's delay above were changed from #10 to #25, at what time would the main thread resume?
Show Answer
20ns. join_any resumes the parent as soon as the fastest thread finishes — with the delays now 20, 30, and 25, the fastest is Thread1_0 at 20ns.
Nested fork...join_any
module tb;
initial begin
$display ("[%0t] Main Thread: Fork join going to start", $time);
fork
fork
print (20, "Thread1_0");
print (30, "Thread1_1");
join_any
print (10, "Thread2");
join_any
$display ("[%0t] Main Thread: Fork join has finished", $time);
end
// Note that this task has to be automatic
task automatic print (int _time, string t_name);
#(_time) $display ("[%0t] %s", $time, t_name);
endtask
endmodule
Output
ncsim> run [0] Main Thread: Fork join going to start [10] Thread2 [10] Main Thread: Fork join has finished [20] Thread1_0 [30] Thread1_1 ncsim: *W,RNQUIE: Simulation is complete. ncsim> exit
The inner fork...join_any is itself treated as a single unit by the outer fork — it's considered "done" as soon as any one of its own two threads finishes. Thread1_0 finishes first among the inner pair, at 20ns, so the inner block is done at 20ns. The outer join_any then resumes as soon as the faster of its two threads finishes: the inner block (done at 20ns) or Thread2 directly (done at 10ns) — and 10ns wins, so the main thread resumes there, exactly as in the non-nested example. Thread1_1, still running inside the inner block, keeps going in the background and prints at 30ns regardless.
Quick Check
Q: If Thread2's delay in the nested example above were changed from #10 to #25, at what time would the main thread resume?
Show Answer
20ns. The inner block is still done at 20ns (Thread1_0 finishes first among the inner pair), and the outer join_any now compares that 20ns against Thread2's new 25ns — 20ns is faster, so the main thread resumes there instead.
Where Is fork...join_any Used?
fork...join_any is the standard way to implement a timeout in a testbench: fork a thread that waits for a specific event alongside a second thread that's just a fixed #delay, and whichever one finishes first — the event actually occurring, or the timeout elapsing — determines what happens next, without risking the simulation hanging forever if the event never comes.
Common Beginner Mistakes
Mistake #1: Assuming join_any waits for every thread, like plain join
join_any only waits for the first thread to finish, not all of them. Why? If you actually need to wait for every spawned thread, that's plain fork...join — using join_any when you meant join means your code continues far earlier than intended, while the "unfinished" threads are still quietly running.
Mistake #2: Forgetting that the remaining threads keep running
Once join_any lets the parent resume, every thread that hasn't finished yet keeps running independently in the background — nothing stops them automatically. Why? If you specifically need those leftover threads to stop (like the losing side of a timeout race), you have to terminate them explicitly with disable fork, covered later in this chapter.
Mistake #3: Assuming a nested join_any stops its own remaining threads once it's "done"
As the nested example shows, Thread1_1 keeps running and prints at 30ns even though the inner join_any was already considered complete at 20ns. Why? "Done" for a join_any block just means the parent (in this case, the outer fork) is allowed to stop waiting on it — it doesn't terminate whatever's still running inside that block.
Practice Exercise
Challenge: Build a Timeout Pattern
Write a fork...join_any that races a wait (done) statement against a #100 timeout, printing "Event occurred" if done goes high first, or "Timed out" if the 100ns delay elapses first.
Hint
Fork two threads: one that simply does wait (done); then displays a message, and one that does #100; then displays a different message. join_any resumes as soon as whichever one finishes first.
Solution
module tb;
bit done;
initial begin
fork
begin
wait (done);
$display ("[%0t] Event occurred", $time);
end
begin
#100;
$display ("[%0t] Timed out", $time);
end
join_any
end
endmodule
If done goes high before 100ns elapses, the first branch finishes first, join_any resumes right there, and "Event occurred" prints. If 100ns passes with done still low, the second branch finishes first instead, and "Timed out" prints — either way, the simulation never hangs waiting indefinitely for done.
Summary
In this tutorial, you learned:
- Plain
fork...joincan hang a simulation if any spawned thread never finishes;join_anyavoids that by only waiting for the first thread to complete - The main thread resumes as soon as the fastest of the forked threads finishes, not all of them
- Threads that haven't finished yet keep running in the background after the parent resumes — nothing stops them automatically
- A nested
fork...join_anyis itself treated as done once any one of its own threads finishes, which is what the outerjoin_anythen compares against its other threads fork...join_anyis the standard building block for a timeout: racing awaitagainst a fixed delay
Continuing the Threads & Processes Chapter
Continue with SystemVerilog disable fork join to terminate leftover background threads, and SystemVerilog wait fork to explicitly wait for them instead.