Once fork...join_any or fork...join_none lets the parent thread resume while some spawned threads are still running, disable fork is how you clean them up: it kills every active thread that was forked by the currently executing process.

7 min read | Intermediate Level

What You'll Learn

  • What disable fork does, and exactly which processes it terminates
  • Why fork...join_any commonly needs disable fork called afterward
  • What happens to a thread that had already fully completed before disable fork runs
  • The scope of disable fork — it isn't limited to just the most recent fork statement

Why Disable a Fork?

Here's what happens at the start of the example below:

  1. The main thread executes the initial block and reaches a fork...join_any block
  2. It launches three threads in parallel, and waits for any one of them to finish
  3. Thread1 finishes first, since it has the least delay
  4. The main thread resumes

Thread2 and Thread3 are still running even after the main thread has come out of the fork...join_any block:


module tb_top;
  
	initial begin
		// Fork off 3 sub-threads in parallel and the currently executing main thread
		// will finish when any of the 3 sub-threads have finished.
		fork
  
			// Thread1 : Will finish first at time 40ns
			#40 $display ("[%0t ns] Show #40 $display statement", $time);  
  
			// Thread2 : Will finish at time 70ns
			begin
				#20 $display ("[%0t ns] Show #20 $display statement", $time);
				#50 $display ("[%0t ns] Show #50 $display statement", $time);
			end
  
			// Thread3 : Will finish at time 60ns
			#60 $display ("[%0t ns] TIMEOUT", $time);
		join_any
  
		// Display as soon as the fork is done
		$display ("[%0tns] Fork join is done", $time);  
   end
endmodule

Output

Output
ncsim> run
[20 ns] Show #20 $display statement
[40 ns] Show #40 $display statement
[40ns] Fork join is done
[60 ns] TIMEOUT
[70 ns] Show #50 $display statement
ncsim: *W,RNQUIE: Simulation is complete.
ncsim> exit

Even after "Fork join is done" prints at 40ns, Thread3's TIMEOUT message still shows up at 60ns, and Thread2's second display still shows up at 70ns — both threads kept running in the background with no way to stop them, since nothing in this version tells them to.

Quick Check

Q: In the output above, why does "Fork join is done" print at exactly 40ns, and not later?

Show Answer

Because this is fork...join_any, not plain join. The main thread resumes as soon as the first of the three threads finishes — Thread1, at 40ns — without waiting for Thread2 (70ns) or Thread3 (60ns) at all.

What Happens When a Fork Is Disabled?

Taking the same example, disable fork is added right after the fork...join_any finishes:


module tb_top;
  
	initial begin
   		// Fork off 3 sub-threads in parallel and the currently executing main thread
      	// will finish when any of the 3 sub-threads have finished.
		fork
  
         // Thread1 : Will finish first at time 40ns
         #40 $display ("[%0t ns] Show #40 $display statement", $time);  
  
         // Thread2 : Will finish at time 70ns
         begin
            #20 $display ("[%0t ns] Show #20 $display statement", $time);
            #50 $display ("[%0t ns] Show #50 $display statement", $time);
         end
  
         // Thread3 : Will finish at time 60ns
          #60 $display ("[%0t ns] TIMEOUT", $time);
      join_any
  
      // Display as soon as the fork is done
      $display ("[%0tns] Fork join is done, let's disable fork", $time);  
  
      disable fork;
   end
endmodule

Output

Output
ncsim> run
[20 ns] Show #20 $display statement
[40 ns] Show #40 $display statement
[40ns] Fork join is done, let's disable fork
ncsim: *W,RNQUIE: Simulation is complete.
ncsim> exit

Thread2 and Thread3 are killed by disable fork at 40ns — neither the TIMEOUT message nor Thread2's second display ever appears. Thread1, on the other hand, is unaffected: it had already fully completed before disable fork ran, so there was nothing left of it to terminate. Notice Thread2's first display, at 20ns, still shows up too — that statement had already executed before disable fork was called; only the parts of a thread still pending when disable fork runs get cut off.

disable fork terminates every active process that was forked by the currently executing process, directly or indirectly — not just the threads from the single most recent fork statement. If the current process had launched several separate fork...join_any or fork...join_none blocks earlier and left threads running from each of them, a single disable fork call kills all of them at once, not just the ones from whichever fork happens to be textually closest.

Where Is disable fork Used?

disable fork is the natural cleanup step after a fork...join_any timeout pattern — once the race between an event and a timeout has resolved one way or the other, the "losing" thread is still running in the background, and disable fork is how you make sure it actually stops instead of lingering for the rest of the test.

Common Beginner Mistakes

Mistake #1: Forgetting disable fork after join_any or join_none

Threads left running after a fork...join_any or fork...join_none don't stop on their own. Why? Without an explicit disable fork (or wait fork, covered next), those threads can keep executing in the background for the rest of the simulation, which is rarely the intended behavior.

Mistake #2: Assuming disable fork only affects the most recent fork block

As the note above explains, disable fork kills every active descendant process of the current process, not just whichever fork statement appears right before it. Why? If earlier code in the same process left other background threads running from a completely different fork block, those get terminated too — which can be surprising if you only meant to clean up the most recent one.

Mistake #3: Expecting disable fork to undo work a thread already finished

As Thread1 shows in the example above, a thread that already completed before disable fork runs is simply unaffected — its effects (like a display statement, or a signal it already drove) already happened and stay that way. Why? disable fork can only stop work that's still pending; it has no way to reverse something a thread already finished doing.

Practice Exercise

Challenge: Clean Up After a Timeout

Take the wait(done)-versus-#100 timeout pattern from the previous article and add disable fork right after the join_any, so that whichever branch didn't finish first gets terminated instead of lingering.

Hint

Place disable fork; as the very next statement after join_any, outside the fork block itself.

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

    disable fork;
  end
endmodule

If done goes high before 100ns, the first branch finishes, join_any resumes, and disable fork then terminates the still-waiting #100 branch before it ever gets a chance to print "Timed out." If the timeout wins instead, disable fork terminates the branch still stuck on wait (done).

Summary

In this tutorial, you learned:

  • disable fork terminates every active process forked by the currently executing process
  • A thread that has already fully completed before disable fork runs is unaffected — only pending work gets cut off
  • Statements a thread already executed before disable fork runs stay executed; only what's still pending is stopped
  • disable fork's scope covers every active descendant process of the current process, not just the most recent fork statement
  • It's the standard cleanup step after a fork...join_any timeout pattern, terminating whichever branch didn't win the race

Continuing the Threads & Processes Chapter

Continue with SystemVerilog wait fork for explicitly waiting on background threads instead of terminating them.

Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5