Skip to content

SystemVerilog · Module 14

disable fork & wait fork

SystemVerilog disable fork and wait fork — the lifecycle controls for background threads. disable fork kills; wait fork waits; both operate on a process scope, not on a specific thread. The scope-isolation discipline (wrap forks in tasks) is what separates engineers who use these correctly from those who silently kill monitors.

Module 14 · Page 14.4

disable fork kills threads. wait fork collects them. Together they give precise control over the lifecycle of every background process the testbench spawns. But both operate on a scope, not on a specific thread — understanding that scope is what separates engineers who use these correctly from those who silently kill monitors they wanted to keep alive. This page walks the scope semantics, the surgical disable <block_name> alternative, the deadlock failure mode of wait fork with forever threads, and the task-isolation discipline that makes both safe.

1. Engineering Problem — Why disable fork and wait fork Exist

The three fork-join* variants (14.1, 14.2, 14.3) give the testbench three ways to spawn concurrent threads. None of them give the testbench a way to manage the threads' lifecycle after spawning. Two needs remain unmet:

  • Kill threads on demand. After fork-join_any unblocks the parent because the work succeeded, the watchdog thread is still counting and will fire its $fatal later — a false timeout (see 14.2 §10 bug #1). The parent needs a way to terminate the loser immediately.
  • Wait for an unknown number of background threads. After spawning N threads via fork-join_none in a loop, the parent needs a barrier that blocks until all N complete — but fork-join cannot express "wait for N threads where N is a runtime value" (see 14.3 §4.3).

disable fork and wait fork are the IEEE 1800 answers. Both target the child threads of the calling process — every thread the current process has spawned via any fork statement that has not yet terminated. The scope is per-calling-process, not per-fork-statement.

2. Mental Model — Process-Tree Operations

The picture every engineer carries:

A SystemVerilog process is a node in a tree. Every fork statement the process executes adds child nodes — the spawned threads — under it. disable fork is a prune-this-process's-children operation: it cuts the entire subtree below the current process. wait fork is a wait-for-all-my-children-to-finish barrier: it blocks until the subtree below the current process collapses to zero leaves.

Three invariants this picture preserves:

  • Scope is the calling process. Neither disable fork nor wait fork says "this fork" — they say "every child of the process executing me." If the same process spawned five separate fork blocks, all five sets of threads are in scope.
  • Tasks reset the scope. Each task call creates a fresh process. Forks inside the task spawn threads under the task's process, not the caller's. disable fork inside the task kills only the task-call's threads — not the caller's threads. This is the cornerstone discipline of safe lifecycle control.
  • disable fork and wait fork are scope-symmetric. They affect exactly the same set of threads — kill versus wait. wait fork after disable fork is a no-op (the threads are already gone); disable fork after wait fork is a no-op (everything already terminated).

3. Visual Explanation — The disable fork Scope

SystemVerilog fork/join concurrency blockThe trap: disable fork after join_any also kills the unrelated monitorin the same processFORKThread 1Thread 1do_work(); done = 1Thread 2Thread 2repeat(100) @posedgeclkThread 3Thread 3forever monitor()JOIN_ANYwait for ANY one thread

The figure shows the exact scenario that bites every junior. Three threads spawned in the same initial block: a work thread, a watchdog timer, and a forever monitor. After fork-join_any unblocks on the first completion, a disable fork issued from the surrounding initial block kills all three — including the monitor that was supposed to live forever. The fix (§8) is to isolate the work + watchdog pair inside a task automatic; the monitor stays in the outer scope.

4. Syntax & Semantics — disable, wait fork

4.1 disable fork — kill all child threads

SystemVerilog — disable fork: terminates every child thread of the current process
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Syntax ────────────────────────────────────────────────────
disable fork;        // kills ALL child threads of the current process
 
// ── Basic example: the canonical timeout idiom ────────────────
bit work_done = 0;
 
fork
    begin : work_thread
        do_long_operation();
        work_done = 1;
    end
    begin : timeout_thread
        repeat (1000) @(posedge clk);
    end
join_any
disable fork;        // kills whichever thread did NOT finish first
 
if (work_done) $display("[TB] Done in time");
else           $fatal(1, "[TB] TIMEOUT");
 
// ── After fork-join_none: kill all background threads ─────────
fork
    forever monitor_apb();
    forever monitor_axi();
join_none
 
run_stimulus();
// ... test body ...
 
disable fork;        // kills both monitors when test is complete
$finish;

4.2 disable <block_name> — surgical single-thread kill

When disable fork's shotgun semantics are wrong, name the specific thread and kill it individually.

SystemVerilog — disable <block_name>: kill exactly one named thread
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Name the blocks you want to be able to kill ───────────────
fork
    begin : monitor_apb        // named block
        forever sample_apb();
    end
    begin : monitor_axi        // named block
        forever sample_axi();
    end
    begin : stimulus
        send_test_traffic();   // finite
    end
join_any
 
// Kill ONLY the stimulus thread — keep both monitors alive
disable stimulus;
// OR: kill only one monitor while keeping the other
disable monitor_axi;
 
// ── disable can target any named block, not just fork threads ─
begin : outer_loop
    for (int i = 0; i < 100; i++) begin
        process_item(i);
        if (early_exit_condition)
            disable outer_loop;   // jumps out of the named block immediately
    end
end
// Execution continues here after disable outer_loop

disable <name> is the right tool when an agent wants to stop its threads without nuking the testbench-wide monitor pool — see §8.3.

4.3 wait fork — barrier for child threads

wait fork is the inverse of disable fork: instead of killing threads, it blocks the current process until every child thread completes. The natural companion to fork-join_none loops.

SystemVerilog — wait fork: blocks until every in-scope child thread completes
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Syntax ────────────────────────────────────────────────────
wait fork;           // blocks until ALL child threads of current process complete
 
// ── Standard use: N-thread spawn then barrier ─────────────────
for (int i = 0; i < 8; i++) begin
    automatic int ch = i;
    fork
        drive_channel(ch);
    join_none
end
wait fork;           // blocks here until all 8 channel threads are done
$display("[TB] All 8 channels complete");
 
// ── Timeout-protected wait fork ───────────────────────────────
fork
    wait fork;                              // wait for N threads
    #1_000_000 $fatal(1, "Timeout waiting for channels");
join_any
disable fork;        // cancel whichever didn't win
 
// ── wait fork with zero background threads: returns immediately
wait fork;           // if no child threads are running, returns instantly

5. Simulation View — Scope Semantics in Detail

Every SV process has a notion of "my children." When a process executes fork ... join*, every spawned thread becomes a child of that process. The relationship is recorded in the simulator's process tree.

  • disable fork walks the children list, immediately terminates every still-running child, and removes them from the tree. The calling process is unaffected — only its children. Returns instantly (no time advance).
  • disable <block_name> walks the named-block index and terminates exactly the one block named. Returns instantly.
  • wait fork walks the children list and blocks the calling process until the children list is empty. As children terminate, they are removed from the list; when the count reaches zero, the wait unblocks.
  • A task call is a process boundary. When task T is called, a new process is created for the call. Forks inside T spawn children under T's process — not the caller's. disable fork / wait fork inside T operate on T's children only.

The "task as scope boundary" rule is the entire reason the timeout idiom requires a task wrapper: it isolates the disable fork so it doesn't nuke threads the caller spawned elsewhere.

6. Waveform — disable fork Cleanly Killing the Watchdog

The waveform below shows the canonical timeout pattern: a work thread races a watchdog. Work finishes at cycle 4; join_any unblocks the parent; disable fork immediately kills the still-running watchdog at cycle 4 (its 10-cycle timer never reaches its $fatal). The parent proceeds normally.

Figure — canonical timeout idiom: work wins, disable fork kills the watchdog

12 cycles
Figure — canonical timeout idiom: work wins, disable fork kills the watchdogwork done → join_any → disable fork kills watchdogwork done → join_any →…watchdog $fatal would have fired here — avoidedwatchdog $fatal would …clkworkRUNRUNRUNRUNdonedonedonedonedonedonedonedonewatchdogRUNRUNRUNRUNKILLgonegonegonegonegonegonegonemonitorRUNRUNRUNRUNRUNRUNRUNRUNRUNRUNRUNRUNparentBLOCKEDBLOCKEDBLOCKEDBLOCKEDRESUMERUNRUNRUNRUNRUNRUNRUNt0t1t2t3t4t5t6t7t8t9t10t11
At cycle 0 the parent forks work + watchdog + monitor. Work finishes at cycle 4 → join_any unblocks the parent. The disable fork on the next line immediately kills the still-running watchdog (BEFORE it can fire its $fatal at cycle 10). Note the monitor row stays RUN throughout — in this trace the fork is inside a task, so the outer-scope monitor is not in disable fork's scope.

The monitor row stays RUN throughout because the work + watchdog fork is inside a task automatic — the disable fork inside the task is scoped to the task's children, not the outer initial's monitor. Without that task wrapper, the monitor row would flip to KILL at cycle 4 too — the bug §11 #1 catches.

7. Synthesis — Not Applicable

disable fork and wait fork are procedural simulation constructs. They have no hardware footprint and no place in synthesisable RTL — synthesis tools reject them outright. This section is intentionally omitted; the topic does not warrant it.

The hardware idiom for "stop this activity" is just deasserting an enable; for "wait for completion" it is sampling a done flag. Both are RTL semantics, not procedural primitives.

8. Verification View — The Canonical Patterns

8.1 The complete timeout idiom — always wrap in a task

The single most-used idiom in real testbenches. The task automatic wrapper is non-negotiable — it isolates disable fork's scope.

SystemVerilog — production-quality reusable timeout wrapper
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Reusable timeout wrapper — every shop has one ─────────────
task automatic run_with_timeout(
    input int max_cycles,
    ref   bit timed_out
);
    timed_out = 0;
    fork
        begin : work
            do_operation();
        end
        begin : timer
            repeat(max_cycles) @(posedge clk);
            timed_out = 1;
        end
    join_any
    disable fork;   // safe — scoped to this task call only
endtask
 
// ── Variant: wait on an event with a timeout ──────────────────
task automatic wait_for_event_or_timeout(
    ref event done_ev,
    input int max_cycles,
    ref bit   success
);
    success = 0;
    fork
        begin
            @done_ev;
            success = 1;
        end
        begin
            repeat(max_cycles) @(posedge clk);
        end
    join_any
    disable fork;
endtask
 
// ── Usage ─────────────────────────────────────────────────────
event transfer_done;
bit   ok;
 
wait_for_event_or_timeout(transfer_done, 500, ok);
if (!ok) $error("DMA transfer did not complete within 500 cycles");

Without the task automatic wrapper, the disable fork would kill every in-scope thread the caller spawned earlier — including monitors, clock generators, and coverage collectors.

8.2 The N-thread barrier — fork-join_none loop in a task with wait fork

SystemVerilog — variable-N barrier with task-isolated wait fork
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Spawn N channels, wait for all (N is a runtime value) ──────
task automatic run_all_channels(int n_ch);
    for (int i = 0; i < n_ch; i++) begin
        automatic int ch = i;
        fork drive_channel(ch); join_none
    end
    wait fork;   // safe — scoped to this task's children only
                 // Monitors spawned by the caller are NOT in scope.
endtask
 
initial begin
    // Launch persistent monitors (outer scope — NOT inside the task)
    fork
        forever monitor_apb();
        forever monitor_axi();
    join_none
 
    // The N-thread work runs inside a task; the task's wait fork
    // waits ONLY for its own children, not for the monitors.
    run_all_channels(8);
 
    $display("[TB] All 8 channels complete");
end

This is the canonical structure: persistent infrastructure in the outer scope, lifecycle-controlled work in a task. The task wrapper is what makes the patterns from 14.2 and 14.3 compose safely.

8.3 Controlled agent shutdown — named-block disable

disable <block_name> is the right tool when an agent owns several threads and needs to stop them without affecting other agents.

SystemVerilog — class agent with named threads for surgical shutdown
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class ApbAgent;
    string name;
 
    function new(string n); name = n; endfunction
 
    task run();
        fork
            begin : drive_blk
                $display("[%s] Driver started", name);
                forever drive_next_txn();
            end
            begin : mon_blk
                $display("[%s] Monitor started", name);
                forever sample_response();
            end
        join_none
    endtask
 
    task stop_drive();
        disable drive_blk;   // kill only this agent's driver
        $display("[%s] Driver stopped", name);
    endtask
 
    task stop_all();
        disable drive_blk;
        disable mon_blk;     // kill exactly this agent's two threads
    endtask
endclass
 
// ── Coordinated multi-agent shutdown ──────────────────────────
ApbAgent agents[4];
initial begin
    foreach (agents[i]) agents[i] = new($sformatf("APB_%0d", i));
    foreach (agents[i]) begin
        automatic int idx = i;
        fork agents[idx].run(); join_none
    end
 
    run_test_stimulus();
    repeat(50) @(posedge clk);   // drain
 
    foreach (agents[i]) agents[i].stop_all();   // each agent kills only its own threads
    $finish;
end

Note disable drive_blk targets the named block within the agent's run() call. If two ApbAgent instances each have a drive_blk block, disable drive_blk issued from agent A's stop_all() kills only A's drive_blk — not B's. Named-block disable respects the surrounding process tree.

8.4 The nested-task pattern — universal scope isolation

The safest testbench-level structure: every fork that needs disable fork or wait fork lives inside its own task. Each task call is a process boundary; lifecycle controls inside the task affect only that task call's children.

SystemVerilog — production testbench structure with scope-isolated tasks
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Task 1: stimulus with an internal timeout ─────────────────
task automatic timed_stimulus(int cycles);
    fork
        send_packets(100);
        repeat(cycles) @(posedge clk);
    join_any
    disable fork;   // SAFE: only kills this task's children
endtask
 
// ── Task 2: parallel N-channel spawn ──────────────────────────
task automatic run_all_channels(int n);
    for (int i = 0; i < n; i++) begin
        automatic int ch = i;
        fork drive_channel(ch); join_none
    end
    wait fork;   // SAFE: only waits for this task's N children
endtask
 
// ── Top-level initial block: clean separation ─────────────────
initial begin
    // Persistent monitors live in the outer scope — NOT in any task
    fork
        forever monitor_apb();
        forever monitor_axi();
    join_none
 
    // Lifecycle-controlled work runs in tasks
    timed_stimulus(500);          // disable fork inside — monitors survive
    run_all_channels(8);          // wait fork inside — monitors not waited on
 
    repeat (50) @(posedge clk);   // drain
    $finish;                       // monitors killed by $finish — intended
end

This pattern is the right answer to "how do I structure a testbench that has persistent monitors AND timeouts AND variable-N parallel work?"

9. Industry Usage — Where disable / wait fork Land in Real Verification

  • UVM phase.drop_objection shutdown — the canonical UVM way to end a test phase. Internally, uvm_phase uses disable fork to terminate the run-phase tasks of every component when the last objection drops. Understanding the raw mechanism makes UVM's "test ends but my monitor's last sample is lost" failures debuggable.
  • UVM uvm_objection::set_drain_time() — the drain-window pattern from 14.3 §8.4 is exactly the same idea: give background threads a window before they're killed. The drain time becomes a repeat(N) @(posedge clk); before the implicit disable fork at phase end.
  • UVM uvm_sequence::kill() — kills the currently-running sequence body via disable on a named block inside the sequencer.
  • Reusable verification IP timeout wrappers — every shop has a run_with_timeout(task, cycles) macro / task built on fork-join_any + disable fork inside a task wrapper. The pattern is in every protocol VIP, every reusable scoreboard helper, every regression infrastructure module.
  • Reset-injection sequences — randomised reset injectors run as background fork-join_none threads. When the test ends, a disable reset_injector; cleanly stops the injector without nuking other monitors. The named-block discipline matters.
  • Multi-clock domain testbenches — each clock generator runs in its own forever thread, named individually so a CDC test can disable fast_clk; to stop one domain without affecting another.
  • Coverage drain windows — the wait fork pattern at end of test, scoped to a task wrapping the parallel sample-points spawned per channel, ensures every coverage point gets its final sample before the next phase begins.

10. Design Review Notes — What a Senior Will Flag

Pattern in the diffWhat review will say
disable fork at top level of an initial block where monitors were also spawned"Scope bug — disable fork kills the monitors too. Move the timeout into a task automatic wrapper so the disable is scoped to the task's children, not the initial block's."
wait fork; at top level of an initial block where a forever monitor was spawned"Deadlock — wait fork blocks until every child completes, including the forever. Same fix: move the N-thread spawn into a task automatic."
disable fork after fork-join_any without a task automatic wrapper"Either confirm there are no other in-scope spawns from this process, or wrap in a task. Default to the task wrapper — it costs one line and prevents an entire class of regressions."
Anonymous fork branches when the comment says "we'll disable this later""Name the branches — disable <name> requires a name. Anonymous can only be killed by disable fork, which is nuclear. Naming is free; not naming is a debt."
disable fork; followed by wait fork;"Redundant — disable fork already terminated everything, so wait fork has nothing to wait on. Remove one; if the author wanted 'kill and wait for them to actually stop,' note that disable fork is already synchronous."
task automatic foo(); fork ... disable fork; endtask with no comment explaining the scope rationale"Add a one-line comment: // disable fork scoped to this task call; outer monitors safe. The scope choice is not obvious; document it."
Class agent with task stop(); /* empty */ endtask and no disable of named blocks"stop() does nothing — the spawned threads outlive the call. Name the agent's blocks and disable <name> each in stop(). See 14.3 §9."
Nested forks inside a task with disable fork at the outer level"disable fork recurses into nested forks within the same process — confirm this is the intent. If you want to kill only the outer fork's direct children, name them and disable <name> selectively."
wait fork; immediately after $finish is reached on the foreground path"Dead code — $finish kills everything before wait fork runs. Remove."

The single highest-value rule: every disable fork and every wait fork belongs inside a task automatic. The task wrapper is the scope boundary that makes both safe to compose with persistent monitors. Code-review's default question is: "is this disable / wait fork inside a task?" — and if not, "why not?".

11. Debugging Guide — Real Failures, Real Fixes

1

Test passes then immediately stops monitoring; coverage shows zero samples after t=N

DISABLE-FORK-KILLED-MONITOR
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initial begin
    fork forever monitor_apb(); join_none
    fork forever monitor_axi(); join_none
 
    fork
        do_work();
        repeat(100) @(posedge clk);
    join_any
    disable fork;            // BUG: kills BOTH monitors too
    repeat(50) @(posedge clk);
    $finish;
end
Symptom
Test reports complete. APB and AXI coverage show transactions up to the moment do_work() finished, then zero transactions for the next 50 cycles of drain. Scoreboard reports "0 transactions to check" for those final cycles.
Root Cause
disable fork killed all child threads of the current process — including both forever monitors spawned earlier. The drain window had nothing watching the bus; the final transactions went uncaptured.
Fix
Wrap the timeout fork in a task automatic: task automatic timed_work(int cycles); fork do_work(); repeat(cycles) @(posedge clk); join_any disable fork; endtask. The disable is now scoped to the task call's children — the outer-scope monitors are untouched.
2

Test hangs at end after the N-channel loop; watchdog eventually fires

WAIT-FORK-FOREVER-MONITOR
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initial begin
    fork forever monitor(); join_none
 
    for (int i = 0; i < 8; i++) begin
        automatic int ch = i;
        fork drive_channel(ch); join_none
    end
    wait fork;               // BUG: includes the forever monitor
    $finish;
end
Symptom
All 8 channels complete (visible in the log). Then silence until the regression watchdog fires at 100 ms. Stack trace shows the test stuck on the wait fork; line.
Root Cause
wait fork blocks until every child of the current process completes. The forever monitor() is a child of this initial block and never completes — wait fork blocks indefinitely.
Fix
Move the N-channel spawn into a task automatic run_channels(int n); for ...; wait fork; endtask. The wait fork inside the task waits only for the task call's 8 children; the outer-scope monitor is not in its scope.
3

agent.stop() returns but threads keep running for many more cycles

STOP-WITHOUT-DISABLE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class ApbAgent;
    task run();
        fork
            forever drive_next();
            forever sample_response();
        join_none
    endtask
 
    task stop();
        // empty — author thought "running = 0" would suffice
    endtask
endclass
 
ApbAgent a = new();
initial begin
    fork a.run(); join_none
    do_test();
    a.stop();            // returns immediately; threads keep running
    $finish;
end
Symptom
After a.stop() the log keeps printing [AGENT] drive_next messages for tens of cycles. Subsequent tests collide with the still-active driver.
Root Cause
stop() did nothing — the threads were anonymous and stop() had no way to kill them. The threads outlived the stop() call until $finish finally took them out.
Fix
Name the forked blocks (begin : drive_blk forever ... end) and disable drive_blk; / disable mon_blk; inside stop(). The named-block disable terminates exactly the agent's two threads. Pattern documented in §8.3.
4

Cross-simulator timeout behaviour: VCS clean, Questa fails

UNDEFINED-DISABLE-SCOPE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fork
    begin
        inner_task();        // inner_task spawns its own fork-join_none
        disable fork;        // ambiguous: includes inner_task's threads?
    end
join_none
Symptom
VCS passes; Questa shows the inner task's threads still running after the outer disable. Tests behave differently on different simulators. Hours wasted suspecting a simulator bug.
Root Cause
disable fork recurses into nested forks within the same process — but whether inner_task's threads are in the same process depends on whether inner_task was declared task automatic (separate process) or plain task (potentially still in the caller's process for some constructs). Different simulators interpret edge cases differently.
Fix
Always declare tasks called from concurrent contexts as task automatic. Then the inner task is unambiguously a separate process; its forks have their own scope; the outer disable fork does not reach them. The disable-fork scope rules become deterministic.
5

Reset injector won't stop at end of test

UNREACHABLE-DISABLE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initial begin
    fork
        begin : reset_injector
            forever begin
                wait_random_interval();
                pulse_reset();
            end
        end
    join_none
 
    run_test();
    $finish;             // BUG: never reaches a disable for the injector
end
// $finish kills the injector — but does it kill the in-flight pulse_reset()?
Symptom
Test passes. Next test in the regression starts at $finish boundary and immediately sees stale reset state — the previous test's reset injector was mid-pulse when $finish killed it; some signals were left in an intermediate state.
Root Cause
$finish is a guillotine, not a clean shutdown. The reset injector's pulse_reset() was mid-execution when $finish ran; signals it had asserted weren't deasserted; the next test inherits the corrupted state.
Fix
Before $finish, call disable reset_injector; to terminate the injector at a known point. Better: add a drain step inside the injector that respects a running flag and exits cleanly: forever begin if (!running) break; wait_random_interval(); pulse_reset(); end. Combined with disable reset_injector for the hard stop, the test cleans up deterministically.

12. Interview Insights — What Interviewers Actually Probe

disable fork kills all threads that are children of the calling process — every thread spawned by any fork statement executed by the current process that has not yet completed. The scope is per-calling-process, not per-fork-statement: if the same initial block spawned five separate fork blocks, all five sets of threads are in scope. To limit scope, wrap the fork in a task automatic; the disable inside the task affects only the task call's children.

13. Exercises

1. Design — reusable timeout task (Foundation)
Write a task automatic run_with_event_timeout(ref event done_ev, input int max_cycles, ref bit timed_out) that races @done_ev against repeat(max_cycles) @(posedge clk), sets timed_out correctly, and uses disable fork safely. Confirm the implementation doesn't kill any threads spawned by the caller.

2. Debug — the disappearing monitor (Intermediate)
A teammate's test passes, but coverage shows zero APB transactions for the last 100 cycles of the drain window. The testbench has fork forever monitor_apb(); join_none in the initial block, followed by a fork do_work(); repeat(500) @(posedge clk); join_any disable fork;. Identify the bug; write the one-line architectural change that fixes it.

3. Code review — the silent agent (Intermediate)
An ApbAgent class has task run(); fork forever drive(); forever monitor(); join_none endtask and task stop(); /* empty */ endtask. Identify the bugs in stop(), propose the canonical fix using named blocks + disable, and explain why disable fork inside stop() would NOT be a correct alternative.

4. Trade-off — disable fork vs disable <block_name> (Advanced)
A junior teammate argues: "always use disable fork — it's simpler and shorter. The task wrapper handles scope." Argue the case against the blanket rule. Construct a real-world scenario (multi-agent testbench) where the agent must stop its own threads without affecting other agents' threads in the same task call — and show why disable <block_name> is required there.

14. Summary

disable fork kills all child threads of the calling process. wait fork blocks until they all finish. Both operate on the process scope, not on a specific fork statement — and the calling process accumulates children from every fork it ever executed. The discipline that makes both safe is wrapping every lifecycle-controlled fork inside a task automatic: the task creates a new process boundary, and disable fork / wait fork inside the task scope only the task call's children.

Defaults to memorise. fork-join_any + disable fork is the timeout idiom — always inside a task. fork-join_none loop + wait fork is the variable-N barrier — always inside a task. disable <block_name> is surgical: name long-lived threads so you can stop them individually without nuking the whole scope. Never call wait fork when any child is a forever loop. Persistent monitors live in the outer scope, never inside the task that runs the lifecycle-controlled work.

This completes the fork-lifecycle controls. With the three fork-join* variants (14.1, 14.2, 14.3) and disable / wait fork, you have every primitive Module 14 covers except the explicit process class — which exposes thread state and gives even finer-grained control.

Next up: 14.5 — The process Class — the SystemVerilog mechanism for capturing a thread handle, querying its state (FINISHED / RUNNING / WAITING / SUSPENDED / KILLED), and suspending / resuming / killing individual threads via the handle. The final tool in the fork-lifecycle toolbox.