fork join vs join_any vs join_none in SystemVerilog (With Timing Diagram)

In short: fork starts several threads at the same time. The keyword that closes it decides when the parent moves on. join waits until all threads finish. join_any waits until the first one finishes, and the others keep running. join_none doesn’t wait at all; the threads start as soon as the parent pauses. Use wait fork to wait for running children later, and disable fork to stop them.

A testbench does many things at once.

Drive stimulus. Watch the bus. Check responses. Run a timeout.

fork is how you start those things in parallel. The only real question is: when does the code after the fork run?

Think of it like a restaurant kitchen

The head chef (the parent thread) hands three orders to three cooks (the child threads).

  • **join: the chef waits until all three** dishes are ready, then carries them out together. A table that ordered together is served together.
  • **join_any: the chef carries out the first** dish that’s ready. The other cooks keep cooking.
  • **join_none:** the chef hands over the orders and goes straight back to the front to take the next table. The cooks start the moment the chef walks away.

And two more tools:

  • **wait fork:** the chef stands at the pass and waits for every cook still working.
  • **disable fork:** the chef shouts “stop, cancel those orders!” to the cooks still working.

The three forms in code

Three workers take 10, 20 and 30 ns. Watch when the parent continues.

// Three threads that take 10, 20 and 30 ns. When does the parent move on?
module tb;
  task automatic worker(string name, int delay);
    #delay $display("%0t ns: %s done", $time, name);
  endtask

  initial begin
    $display("--- fork...join");
    fork
      worker("A", 10);
      worker("B", 20);
      worker("C", 30);
    join
    $display("%0t ns: parent continues", $time);

    $display("--- fork...join_any");
    fork
      worker("A", 10);
      worker("B", 20);
      worker("C", 30);
    join_any
    $display("%0t ns: parent continues", $time);
    wait fork;                         // let B and C finish before the next demo

    $display("--- fork...join_none");
    fork
      worker("A", 10);
      worker("B", 20);
      worker("C", 30);
    join_none
    $display("%0t ns: parent continues", $time);
    wait fork;
    $finish;
  end
endmodule

Output (tested on Verilator 5.020, 11 Nov 2026):

--- fork...join
10 ns: A done
20 ns: B done
30 ns: C done
30 ns: parent continues
--- fork...join_any
40 ns: A done
40 ns: parent continues
50 ns: B done
60 ns: C done
--- fork...join_none
60 ns: parent continues
70 ns: A done
80 ns: B done
90 ns: C done

Read each block against its start time:

  • join started at 0. The parent continued at 30 ns, when the slowest worker finished.
  • join_any started at 30. The parent continued at 40 ns, when A finished. B and C kept going to 50 and 60.
  • join_none started at 60. The parent continued at 60 ns, right away. The workers finished later.
Timing diagram from the tested run: with workers of 10, 20 and 30 ns, the parent continues after 30 ns with join, 10 ns with join_any and 0 ns with join_none
Figure 1. When the parent continues: after all workers (join), the first (join_any) or none (join_none). Drawn from the tested run.

fork join vs join_any vs join_none

fork...joinfork...join_anyfork...join_none
Parent continues whenAll threads finishThe first thread finishesImmediately
Other threads after the parent moves onNone leftKeep runningAll keep running
In the run above (10/20/30 ns workers)After 30 nsAfter 10 nsAfter 0 ns
Kitchen pictureServe the whole table togetherServe the first dishHand over orders, go back to the front
Typical useDrive and monitor, then checkResponse vs timeoutStart background monitors, spawn N parallel jobs
Allowed in a function?NoNoYes (it doesn’t block)

This is IEEE 1800-2017, section 9.3.2.

The loop-variable trap with join_none

This is the most-asked fork question on the forums. You fork one thread per loop pass, and every thread sees the last value of the loop variable.

Why? With join_none, the children don’t start until the parent pauses. By then the loop has finished, and i already holds its final value. Every child reads the same i (IEEE 1800-2017, 9.3.2).

In kitchen terms: the chef writes “table number” on a single whiteboard and changes it for each order. The cooks only look at the board after the chef has left, and they all read the last number.

Wrong: three join_none threads share loop variable i and all read 3. Fixed: each pass makes an automatic copy k, so threads read 0, 1 and 2
Figure 2. The loop-variable trap: one shared whiteboard vs one copy per pass.
// The loop-variable trap: every forked thread sees the final value of i.
module tb;
  initial begin
    $display("--- wrong");
    for (int i = 0; i < 3; i++)
      fork
        #1 $display("thread sees i = %0d", i);
      join_none
    wait fork;

    $display("--- fixed");
    for (int i = 0; i < 3; i++) begin
      automatic int k = i;             // a fresh copy for each loop pass
      fork
        #1 $display("thread sees k = %0d", k);
      join_none
    end
    wait fork;
    $finish;
  end
endmodule

What you’ll see:

  • Wrong version: per the standard, all three threads print i = 3. Forum users reported exactly this (every thread got 4 with a 5-pass loop, or 3 with a 3-pass loop). Verilator 5.020 happens to print 0, 1, 2 here, so don’t use it to learn this rule.
  • Fixed version (tested on Verilator 5.020):
thread sees k = 0
thread sees k = 1
thread sees k = 2

The fix is to give each pass its own whiteboard: declare an automatic copy inside the loop body, before the fork.

wait fork and disable fork

  • **wait fork waits for every direct child** the current thread has started, including ones from earlier forks you may have forgotten about.
  • **disable fork** kills every child the current thread has started, and their children too (IEEE 1800-2017, 9.6.1 and 9.6.3).

Both act on the whole family of the current thread, not just the last fork. That’s why the safe pattern wraps them in their own little thread:

fork
  begin : guard            // a new parent with only these children
    fork
      ...
    join_any
    disable fork;          // only touches the children of 'guard'
  end
join

Think of it as giving the chef a separate small kitchen for this job. Shouting “stop!” there doesn’t cancel orders in the main kitchen.

Without a guard, disable fork also kills a background monitor; with a begin guard block it only kills the response and watchdog threads
Figure 3. A begin guard block keeps disable fork away from threads you still need.

Working example 1: wait for a response, with a timeout

Every AXI or APB driver needs this: wait for the response, but don’t hang forever if the slave is stuck.

// Wait for a response, but give up after a timeout. join_any + disable fork.
module tb;
  bit resp_seen;

  task automatic wait_for_response(int resp_delay, int timeout);
    resp_seen = 0;
    fork
      begin : guard                    // keeps disable fork away from other threads
        fork
          begin #resp_delay resp_seen = 1; end   // stands in for "wait for BVALID"
          #timeout;                               // watchdog
        join_any
        disable fork;                  // kill whichever one is still running
      end
    join
    if (resp_seen) $display("%0t ns: response arrived", $time);
    else           $display("%0t ns: TIMEOUT after %0d ns", $time, timeout);
  endtask

  initial begin
    wait_for_response(15, 50);         // fast slave
    wait_for_response(80, 50);         // stuck slave
    $finish;
  end
endmodule

Output (Verilator 5.020):

15 ns: response arrived
65 ns: TIMEOUT after 50 ns
  • The fast slave answered at 15 ns. join_any released the parent, and disable fork killed the 50 ns watchdog.
  • The stuck slave would have taken 80 ns. The watchdog won at 15 + 50 = 65 ns, and disable fork killed the response wait.

Working example 2: continue after any 3 of 5 jobs

Sometimes you need “wait for some, not all”. A semaphore counts the finished jobs.

// Start 5 jobs, continue as soon as any 3 finish, then stop the other 2.
module tb;
  semaphore done = new(0);             // starts with no keys

  task automatic job(int id, int delay);
    #delay $display("%0t ns: job %0d finished", $time, id);
    done.put(1);                       // hand in one key
  endtask

  initial begin
    fork
      begin : guard
        fork
          job(1, 10); job(2, 15); job(3, 5); job(4, 20); job(5, 25);
        join_none
        done.get(3);                   // blocks until 3 keys are in
        $display("%0t ns: 3 jobs done, stopping the rest", $time);
        disable fork;                  // kills jobs 4 and 5
      end
    join
    #50 $display("%0t ns: end of test (jobs 4 and 5 never printed)", $time);
    $finish;
  end
endmodule

Output (Verilator 5.020):

5 ns: job 3 finished
10 ns: job 1 finished
15 ns: job 2 finished
15 ns: 3 jobs done, stopping the rest
65 ns: end of test (jobs 4 and 5 never printed)

Jobs 4 and 5 would have finished at 20 and 25 ns, but they never print. disable fork stopped them at 15 ns.

Common mistakes (and the fix for each)

These come from real Verification Academy threads, most answered by Dave Rich of Siemens. Each is retold in my own words with a link.

1. Using the loop variable inside fork…join_none

  • What happened: in 2013 an engineer forked thread(i) in a 5-pass loop, and every thread got 4 (Verification Academy, 2013). The same question came back in 2021, with every thread printing 3 (Verification Academy, 2021).
  • Why: the children start only after the parent pauses, when the loop has already finished.
  • Fix: automatic int k = i; in the loop body, before the fork, and use k in the thread. Declare and set it in one statement; a separate assignment inside the fork can race.

2. disable fork stopping more than you meant

  • What happened: in 2017 someone used disable fork inside a UVM sequence and asked how far it reaches (Verification Academy, 2017).
  • Why: it kills every child of the thread that runs it, from all earlier forks, not just the last one.
  • Fix: wrap the fork in fork begin ... end join so disable fork only sees the children you mean. Or keep process handles from process::self() and call kill() on just the ones you want.

3. wait fork waiting for a thread you forgot about

  • What happened: in 2017 an engineer was unsure what wait fork waits for, and a reply got it wrong (Verification Academy, 2017).
  • Why: it waits for every direct child still running, including ones started by earlier forks in the same thread. A forgotten background monitor means wait fork never returns.
  • Fix: use the same begin ... end guard around the forks you want to wait for.

4. Thinking join_any stops the other threads

  • What happened: in 2018 someone compared fork...join with fork...join_any plus wait fork and asked if they were the same (Verification Academy, 2018).
  • Why: join_any only releases the parent early. The other threads keep running, which our output shows (B and C finishing at 50 and 60 ns).
  • Fix: if you want them gone, follow join_any with disable fork, inside a guard.

5. Counting finished threads with a shared counter

  • What happened: in 2020 an engineer ran 5 tasks, bumped a shared count in each, waited for count == 3, then disabled the rest. It worked, but only by luck (Verification Academy, 2020).
  • Why: if two tasks finish in the same time step, count can jump from 2 to 4 and wait(count == 3) never fires.
  • Fix: use a semaphore, exactly like working example 2: each job puts a key, the parent gets 3.

Frequently asked questions

What is the difference between join, join_any and join_none?

join waits for all threads, join_any waits for the first one, and join_none doesn’t wait.

Does join_any stop the other threads?

No. They keep running. Add disable fork (inside a guard) to stop them.

When do join_none threads actually start?

When the parent pauses, for example at its next #delay, @ or wait. Not at the join_none line itself.

What does wait fork wait for?

All direct children of the current thread that are still running, from every fork it has done.

What does disable fork kill?

All children of the current thread, and their children. Wrap it in fork begin ... end join to limit it.

Can I use fork inside a function?

Only fork...join_none, because a function can’t wait.

How do I stop just one thread?

Save its handle with process p = process::self(); inside the thread, then call p.kill().

What to read next

References

  • IEEE 1800-2017, SystemVerilog standard: section 9.3.2 (parallel blocks: join, join_any, join_none), 9.6.1 (wait fork), 9.6.3 (disable fork), 9.7 (fine-grain process control), 15.3 (semaphores).
  • Verification Academy forum threads linked in the mistakes section (accessed 11 Nov 2026).
  • Examples 1, 3 and 4 tested on Verilator 5.020 with --timing on 11 Nov 2026. In the loop example, only the fixed version’s output is shown.

8 Comments

Comments are closed.