Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
    • Classes, Objects & Handles
    • Inheritance and Polymorphism
    • Static, Encapsulation & Parameterized Classes
    • Virtual Interfaces: the Class-to-Pins Bridge
    • Events, Semaphores, Mailboxes and Processes
      • Events: Edge Waits and Level Fixes
      • Semaphores: One Key and Many
      • Mailboxes: Typed Pipes with Backpressure
      • Threads and Control: Fork, Disable, Wait, Process
      • Synchronization Patterns: Handshakes, Phasing and Objections
      • Lab: Generator-to-Driver Handshake
    • OOP Patterns for Testbenches
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Events, Semaphores, Mailboxes and Processes
  1. Topics
  2. SystemVerilog
  3. Object-Oriented SystemVerilog for Verification
  4. Events, Semaphores, Mailboxes and Processes
  5. Threads and Control: Fork, Disable, Wait, Process

Part 2 · OOP for Verification · Intermediate

Threads and Control: Fork, Disable, Wait, Process

Fork variants, the loop capture fix, disable fork, wait fork and process.

On this page

    Why this matters

    Three forked children each printed 3 instead of 0, 1 and 2. They ran after the loop had finished and all read the same loop variable.

    After this lesson you can: choose join, join_any or join_none, capture loop values with automatic copies, and stop or wait for threads without killing the wrong ones.

    Three joins, three contracts

    A fork spawns one child per statement. join resumes the parent after all children end, join_any after the first, and join_none at once.

    Children of join_none do not start until the parent next blocks. That deferral is the root of the loop bug in the next section.

    Ending

    Parent resumes

    Children after resume

    join

    After all end

    None left

    join_any

    After the first ends

    Rest keep running

    join_none

    At once

    All run in background

    Where the parent resumes is the whole difference between the three joins.

    Pair each ending with its job: join for lockstep phases, join_any for racing work against a timeout, join_none for daemon monitors that never return.

    The loop capture fix

    Spawning one child per loop iteration hits the famous capture bug. The children start after the loop ends, so every child reads the shared loop variable at its final value.

    The cure is an automatic copy inside the fork, evaluated at spawn time in the parent context. Each child then owns a private value frozen at the right iteration.

    The full bench sits under Testbench below. A copy outside the fork still shares one variable across children in a static context.

    Stopping threads

    disable fork kills every live child of the calling process, including earlier join_none daemons. That blast radius is the usual cause of monitors that silently stop mid-test.

    The isolation idiom shrinks it: wrap the racing pair in fork begin ... end join, so the disable only reaches the pair. wait fork is the matching cleanup barrier before final checks.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // snippet
    // Isolation wrapper: the disable below cannot reach the daemon monitor.
    fork mon_run(); join_none          // daemon, must survive
    fork
      begin
        fork
          main_sequence();
          #1000;
        join_any
        disable fork;                  // kills only the racing pair
      end
    join

    The process class gives a handle to a running thread, so one thread can check on or stop another:

    top_proc.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    timeunit 1ns;      // one time unit for the classes and the module
    timeprecision 1ps;
    
    class worker_pool_c;
      process workers[3];
      task start();
        foreach (workers[i]) begin
          automatic int idx = i;
          fork
            begin
              workers[idx] = process::self();
              forever begin
                #(10 * (idx + 1));
                $display("[%0t] worker %0d tick", $time, idx);
              end
            end
          join_none
        end
      endtask
      task stop_one(int idx);
        if (workers[idx] != null && workers[idx].status() != process::FINISHED)
          workers[idx].kill();
      endtask
    endclass
    module top_proc;
      worker_pool_c pool = new();
      initial begin
        pool.start();
        #100;
        pool.stop_one(1);
        #100;
        if (pool.workers[1].status() != process::KILLED)
          $fatal(1, "FAIL: worker 1 was not killed");
        if (pool.workers[0].status() == process::KILLED || pool.workers[2].status() == process::KILLED)
          $fatal(1, "FAIL: stop_one(1) killed another worker");
        $display("PASS: stop_one(1) killed worker 1 and left workers 0 and 2 running");
        $finish;
      end
    endmodule
    Terminal
    [10000] worker 0 tick
    [20000] worker 1 tick
    [20000] worker 0 tick
    [30000] worker 2 tick
    [30000] worker 0 tick
    [40000] worker 1 tick
    [40000] worker 0 tick
    [50000] worker 0 tick
    [60000] worker 2 tick
    [60000] worker 1 tick
    ... 9 more lines ...
    [120000] worker 0 tick
    [130000] worker 0 tick
    [140000] worker 0 tick
    [150000] worker 2 tick
    [150000] worker 0 tick
    [160000] worker 0 tick
    [170000] worker 0 tick
    [180000] worker 2 tick
    [180000] worker 0 tick
    [190000] worker 0 tick
    PASS: stop_one(1) killed worker 1 and left workers 0 and 2 running
    [200000] worker 0 tick

    Common pitfall

    Two faults dominate: assuming join_any killed the slower children, which keep running into later phases, and registering process::self after a blocking statement so the parent acts on a still-null handle.

    Interview

    Likely questions: what a loop fork prints, whether siblings survive join_any, and when join_none children begin.

    • Deferred start plus one shared variable explains the print.

    • Survivors keep running until disabled or complete.

    • Children begin when the parent next blocks, not at the fork.

    Check yourself A loop forks three children that print the loop index, with no automatic copy. What prints, and why?

    All three print the final value. The children start after the loop ends and share one variable holding that value.

    Check yourself A timeout race uses join_any plus bare disable fork in a task that also spawned a monitor. What breaks?

    The monitor dies too, since it is a child of the same task. Wrap the race in an inner fork block to isolate the disable.

    Testbench

    Predict first The loop below forks three children with join_none, each with automatic int k = i. What do they write into seen[0..2]?

    0, 1 and 2. The automatic copy is made when each child is spawned, so each child keeps its own index even though the children run after the loop ends.

    Three children, three private copies, one ordered check.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    module tb_fork_loop;
      timeunit 1ns;
      timeprecision 1ps;
      int seen[3];
      int errors = 0;
      initial begin
        for (int j = 0; j < 3; j++) seen[j] = -1;
        for (int i = 0; i < 3; i++) begin
          fork
            automatic int k = i;
            begin
              #1;
              seen[k] = k;
            end
          join_none
        end
        #10;
        for (int j = 0; j < 3; j++) if (seen[j] !== j) errors++;
        if (errors == 0) $display("PASS: 3 forked children each kept its own index");
        else $fatal(1, "FAIL: seen=%0d,%0d,%0d", seen[0], seen[1], seen[2]);
        $finish;
      end
    endmodule
    Terminal
    PASS: 3 forked children each kept its own index

    Remember

    • join waits for all, join_any for the first, join_none for none.

    • join_none children start when the parent next blocks: copy loop values first.

    • disable fork kills every child of the caller: wrap races in their own fork.

    Rule of thumb: copy the value at spawn time, and fence every disable fork.

    How this was checked

    Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.

    Related topics

    • Next: Synchronization Patterns

    Previous

    57 / 259

    Next: Synchronization Patterns: Handshakes, Phasing and Objections

    Up next

    Up nextSynchronization Patterns: Handshakes, Phasing and Objections

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Events: Edge Waits and Level Fixes
    • Semaphores: One Key and Many
    • Lab: Generator-to-Driver Handshake