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...join can hang a simulation, and how join_any avoids that
  • How join_any decides exactly when the parent thread resumes
  • How a nested fork...join_any behaves, 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
fork join, join_any and join_none: when the parent thread resumes

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

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

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...join can hang a simulation if any spawned thread never finishes; join_any avoids 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_any is itself treated as done once any one of its own threads finishes, which is what the outer join_any then compares against its other threads
  • fork...join_any is the standard building block for a timeout: racing a wait against 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.

Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5