Why Learn Threads & Processes? A testbench rarely does just one thing at a time — it needs to drive stimulus while watching for a response, race a timeout against an event it's waiting for, or run several independent sequences at once. SystemVerilog handles all of this through processes, and the fork...join family of constructs is how you create, coordinate, and tear down that concurrency deliberately, instead of fighting against it.

Every initial and always block is already its own process, running concurrently with every other one the moment simulation starts — no fork required. What fork...join adds is the ability to spawn additional concurrent processes from within a single block, and this chapter covers the different flavors of that construct, along with the statements used to wait for or terminate the processes it spawns.

6 min read | Intermediate Level

What You'll Learn

  • What a process is in SystemVerilog, and why every initial/always block already runs concurrently
  • Why testbenches specifically need to spawn additional concurrency from within a single block of code
  • The three fork...join variants, and how they differ in when the parent process resumes
  • How to wait for or terminate processes spawned by a fork...join

What Is a Process?

Once simulation starts, every initial block and every always block is already an independent process, running concurrently with every other process in the design and testbench — this happens automatically, with no special syntax needed. "Process" and "thread" are used more or less interchangeably in this context.

Why Do Testbenches Need Explicit Concurrency?

A single initial block executes sequentially, one statement after another, in program order. Plenty of verification tasks genuinely need to happen at the same time within that one block's logical flow, though — driving a signal while simultaneously watching for a response, or racing a timeout against an event you're waiting for. fork...join spawns multiple concurrent child processes from within a single block, instead of requiring a separate module-level initial/always block for every concurrent task.

The fork...join Family

All three variants spawn the same kind of child processes — they differ only in when the parent process (the one that executed the fork) is allowed to resume:

Quick Check

Q: You want to implement a timeout: wait for a specific event, but give up and move on if a fixed delay elapses first, whichever happens first. Which fork...join variant fits this?

Show Answer

fork...join_any. Spawn the event-wait as one child process and the timeout delay as another; the parent resumes as soon as whichever one finishes first completes, without waiting for the other.

Controlling Spawned Processes

Once a fork...join_any or fork...join_none lets the parent continue while child processes are still running, you sometimes need to explicitly wait for or clean up whatever's left:

  • wait fork — blocks until every child process spawned by the current process has finished. See SystemVerilog wait fork.
  • disable fork — immediately terminates every descendant process spawned, directly or indirectly, by the current process that's still running — reaching further than just its immediate children. See SystemVerilog disable fork join.

Repeating Work Concurrently: the forever Loop

A forever loop is commonly used inside a forked child process to create a persistent background task — a monitor that continuously watches a signal for the entire duration of a test, for example. SystemVerilog forever loop covers this construct in depth.

Where Does This Show Up in a Real Testbench?

Driving stimulus into a DUT while a separate forked process continuously monitors its outputs is one of the most common patterns in verification — as is a timeout watchdog, implemented with fork...join_any, that races a fixed delay against a wait statement so a hung condition doesn't stall an entire regression indefinitely.

Common Beginner Mistakes

Mistake #1: Assuming a forked block always returns immediately

Only fork...join_none returns immediately. Why? Plain fork...join blocks until every child finishes, and fork...join_any blocks until at least one does — assuming either behaves like join_none leads to code that silently waits far longer than expected.

Mistake #2: Leaving spawned processes running at the end of a test

A process spawned by fork...join_any or fork...join_none keeps running in the background even after the parent moves on. Why? If a test finishes without explicitly waiting for or disabling those leftover processes, they can keep executing (or leave dangling activity) right up to $finish, which is rarely the intended behavior.

Mistake #3: Sharing a variable between concurrent processes with no synchronization

Two processes spawned by the same fork...join run concurrently, so if both read and write the same variable with no coordination, the result depends on simulator scheduling order. Why? This is a classic race condition — the Interprocess Communication chapter covers events, semaphores, and mailboxes, which exist specifically to coordinate concurrent processes safely.

Practice Exercise

Challenge: Match the Scenario to the Right Construct

For each scenario below, name the fork...join variant (or process-control statement) that fits best:

  1. Drive three independent stimulus sequences in parallel, and don't move on to the next test step until all three have completed.
  2. Kick off a background monitor that should keep running for the rest of the test, without the parent process waiting for it at all.
  3. A parent process spawned several background tasks earlier with fork...join_none; now, right before the test ends, it needs to make sure every one of them has actually finished.
Hint

Match each scenario against how long the parent process needs to wait: forever (until everything finishes), not at all, or a later explicit checkpoint.

Solution
  1. fork...join — the parent needs all three sequences to finish before continuing.
  2. fork...join_none — the parent continues immediately, and the monitor keeps running independently in the background.
  3. wait fork — this blocks until every child process spawned by the current process (including the earlier join_none ones) has completed.

Summary

In this tutorial, you learned:

  • Every initial/always block is already its own process, running concurrently with every other one, automatically
  • fork...join spawns additional concurrent child processes from within a single block of code
  • The three variants — join, join_any, and join_none — differ only in when the parent process is allowed to resume
  • wait fork blocks until all of the current process's immediate children finish; disable fork reaches further, terminating every descendant — children, grandchildren, and so on — immediately
  • Sharing a variable between concurrently running processes without synchronization is a common source of race conditions

Continuing the Threads & Processes Chapter

Continue with SystemVerilog fork join, SystemVerilog Threads, SystemVerilog forever loop, fork join_none, fork join_any, disable fork join, and wait fork.

Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5