SystemVerilog supports parallel, concurrent threads through the fork...join construct. Multiple procedural blocks can be spawned off at the same time using fork and join, and different variations of the construct (covered later in this chapter) let the parent thread continue at different points depending on when the spawned threads finish. This article covers the plain fork...join form, which waits for every spawned thread to complete.

7 min read | Intermediate Level

What You'll Learn

  • The basic fork...join syntax, and what counts as a single spawned thread
  • How long the parent thread stays suspended at a fork...join
  • How nested fork...join blocks resolve their timing
  • Why a task called concurrently from multiple threads needs to be declared automatic

Syntax


fork
	// Thread 1
	// Thread 2
	// ...
	// Thread 3
join
fork join, join_any and join_none: when the parent thread resumes

A Basic fork...join Example

In the example below, three threads are forked using fork...join. The main thread stays suspended until every thread spawned by the fork completes. Any block of code within begin and end counts as a single thread — here, that's Thread2:


module tb;
	initial begin
      $display ("[%0t] Main Thread: Fork join going to start", $time);
		fork
			// Thread 1
			#30 $display ("[%0t] Thread1 finished", $time);

			// Thread 2
          	begin
              	#5 $display ("[%0t] Thread2 ...", $time);
				#10 $display ("[%0t] Thread2 finished", $time);
            end
  
            // Thread 3
			#20 $display ("[%0t] Thread3 finished", $time);
		join
      $display ("[%0t] Main Thread: Fork join has finished", $time);
	end
endmodule

Output

Output
ncsim> run
[0] Main Thread: Fork join going to start
[5] Thread2 ...
[15] Thread2 finished
[20] Thread3 finished
[30] Thread1 finished
[30] Main Thread: Fork join has finished
ncsim: *W,RNQUIE: Simulation is complete.

All three threads are forked at time 0ns. Thread2 is a block of procedural code that finishes only after executing every statement inside its begin/end, so it completes at 5ns + 10ns = 15ns — the first thread to finish. Thread3 finishes at 20ns, and Thread1 takes the longest, finishing at 30ns. Since plain fork...join waits for every thread, the main thread doesn't resume until 30ns — whichever thread finishes last.

Quick Check

Q: If Thread1's delay above were changed from #30 to #8, at what time would the main thread resume after the join?

Show Answer

20ns. fork...join always waits for the slowest thread to finish — with Thread1 now finishing at 8ns, Thread2 at 15ns, and Thread3 at 20ns, the latest of the three is Thread3 at 20ns.

Where Is fork...join Used?

fork...join is a natural fit for synchronization points in a testbench — spinning up several independent stimulus generators or agents in parallel, then waiting for every one of them to finish before moving on to checking results or starting the next test phase.

Nested fork...join

A fork...join can be nested inside another fork...join.

Example #1


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
          print (10, "Thread2");  
		join
      $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
[20] Thread1_0
[30] Thread1_1
[30] Main Thread: Fork join has finished
ncsim: *W,RNQUIE: Simulation is complete.

The outer fork...join spawns two things: the inner fork...join block itself (which spawns Thread1_0 and Thread1_1 concurrently, finishing once both complete at 30ns) and Thread2 directly (finishing at 10ns). The outer join treats the entire inner block as one of its own threads, so it waits for the later of the two: the inner block's 30ns, versus Thread2's 10ns — landing on 30ns.

print is called concurrently from three separate threads in this example, so it must be declared automatic. Without it, every concurrent call would share a single copy of _time and t_name, and one call could silently overwrite another's arguments mid-execution — automatic gives each call its own independent copy of the task's local variables and arguments.

Example #2


module tb;
	initial begin
      $display ("[%0t] Main Thread: Fork join going to start", $time);
		fork
			fork // Thread 1
              #50 $display ("[%0t] Thread1_0 ...", $time);
              #70 $display ("[%0t] Thread1_1 ...", $time);
              begin
                #10 $display ("[%0t] Thread1_2 ...", $time);
                #100 $display ("[%0t] Thread1_2 finished", $time);
              end
            join

			// Thread 2
          	begin
              	#5 $display ("[%0t] Thread2 ...", $time);
				#10 $display ("[%0t] Thread2 finished", $time);
            end
  
            // Thread 3
			#20 $display ("[%0t] Thread3 finished", $time);
		join
      $display ("[%0t] Main Thread: Fork join has finished", $time);
	end
endmodule

The main thread stays suspended until every nested fork is over, at 110ns:

Output

Output
ncsim> run
[0] Main Thread: Fork join going to start
[5] Thread2 ...
[10] Thread1_2 ...
[15] Thread2 finished
[20] Thread3 finished
[50] Thread1_0 ...
[70] Thread1_1 ...
[110] Thread1_2 finished
[110] Main Thread: Fork join has finished
ncsim: *W,RNQUIE: Simulation is complete.

The inner fork (labeled Thread 1) spawns three threads of its own: one finishing at 50ns, one at 70ns, and a begin/end block finishing at 10ns + 100ns = 110ns. The inner join waits for the slowest of those three — 110ns. The outer join then waits for the slowest of its three threads: the inner block (110ns), Thread2 (15ns), and Thread3 (20ns) — landing on 110ns overall.

Common Beginner Mistakes

Mistake #1: Assuming fork...join lets the main thread continue immediately

Plain fork...join does the opposite — it blocks the parent thread until every spawned thread finishes. Why? If you need the parent to continue as soon as just one thread finishes, or not wait at all, those are different constructs (fork...join_any and fork...join_none, covered later in this chapter) — plain join is the one that waits for everything.

Mistake #2: Forgetting automatic on a task called from multiple concurrent threads

As the note above the print task shows, a task called concurrently by more than one thread needs to be declared automatic. Why? Without it, all concurrent calls share one copy of the task's local variables and arguments, and one thread's call can silently corrupt another's in-progress execution — a bug that's easy to miss since it depends on the exact timing of concurrent calls.

Mistake #3: Assuming a nested fork...join finishes independently of the outer one

The outer join treats an entire nested fork...join block as just one of its own threads. Why? The outer thread doesn't resume until the inner block itself has finished, which means the outer join's total wait time is determined by whichever thread — inner or outer — actually finishes last, not by the outer threads alone.

Practice Exercise

Challenge: Predict the Finish Time

A fork...join spawns three threads: one with a #15 delay, one with a #40 delay, and a begin/end block that waits #5 and then #25 more before finishing. At what simulation time does the main thread resume after the join?

Hint

Work out each thread's own finish time first, then take the latest one — fork...join always waits for the slowest thread.

Solution

40ns. The three threads finish at 15ns, 40ns, and 5ns + 25ns = 30ns respectively. The latest of the three is 40ns, so that's when the main thread resumes.

Summary

In this tutorial, you learned:

  • fork...join spawns multiple threads concurrently, and the parent thread waits until every one of them finishes
  • A begin/end block inside a fork counts as a single thread, finishing only once every statement inside it has executed
  • A fork...join can be nested inside another; the outer join treats the entire inner block as one of its own threads
  • The total wait time for any fork...join is always determined by its slowest thread
  • A task called concurrently from multiple threads must be declared automatic, or concurrent calls will share and potentially corrupt the same local variables
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5