Skip to content

SystemVerilog · Module 14

The process Class

SystemVerilog process class — the precision counterpart to disable fork. Get a handle via process::self(), then kill / suspend / resume / await an exact thread by reference. Five states (RUNNING, WAITING, SUSPENDED, KILLED, FINISHED), six methods, and the canonical agent-with-handle-control pattern.

Module 14 · Page 14.5

disable fork is a blunt instrument — it kills every child thread in the calling process's scope. The process class is the precision tool. It gives the testbench a handle to any specific running thread: kill it individually, pause it, resume it, or wait for it to finish — targeting exactly the thread you mean, leaving every other thread untouched. This page walks the five process states, the six methods, the process::self() registration discipline, and the agent / pool patterns that production verification environments use to manage hundreds of background threads at runtime.

1. Engineering Problem — Why the process Class Exists

14.4 gave the testbench disable fork and wait fork — both operate on a scope (the calling process's children) and both treat every child uniformly. That works for the timeout idiom (kill the loser) and the N-thread barrier (wait for them all). It does not work when:

  • One specific agent of N must be stopped. A 4-channel testbench finds channel 2 misbehaving and wants to disable just that channel's driver. disable fork would kill all four; disable <block_name> requires the block be named at compile time, which doesn't scale to runtime-spawned thread pools.
  • A driver needs to be temporarily paused, not killed. During DUT reset or DFT scan mode, the driver should freeze (state preserved) and resume after — disable is one-way; there is no re-enable fork.
  • A test needs to wait for one specific initialiser thread out of many. wait fork waits for all in-scope children; the test wants to wait for thread 7 specifically while monitors keep running.
  • A health monitor needs to poll the state of every running agent. "Is the AXI driver still alive?" cannot be answered by a fork-statement scope — it requires a handle to the specific thread.

The process class is the IEEE 1800 answer: a built-in class that gives every running thread an addressable handle. Once captured (via process::self() from inside the thread), the handle lets the rest of the testbench act on that exact thread.

2. Mental Model — A Thread Has a Handle

The picture every engineer carries:

Every running thread has exactly one process object — like an OS thread has a thread ID. The thread captures its own handle with process::self(), then publishes the handle so other code can act on it. Without a handle, a thread is anonymous (you can only disable fork it as part of a scope); with a handle, the thread is addressable (you can kill(), suspend(), resume(), or await() it individually).

Three invariants this picture preserves:

  • process::self() is the only constructor. You cannot new() a process — the class has no user-callable constructor. The only way to obtain a handle is to call process::self() from inside the thread whose handle you want.
  • Five states cover every lifecycle moment. RUNNING (actively executing) / WAITING (blocked on #, @, or a blocking call) / SUSPENDED (paused by suspend()) / KILLED (terminated by kill()) / FINISHED (ran to completion). status() reads the state; six methods drive the transitions.
  • Handles outlive the thread. After kill() or natural completion, the handle remains valid — status() returns KILLED or FINISHED. You cannot resume() a dead thread, but you can still query whether it died.

3. Visual Explanation — The Five-State Lifecycle

The process::state enum has five members. Every running thread is in exactly one of them at any moment; methods drive the transitions.

StateMeaningEntered byExited by
RUNNINGActively executing in the schedulerspawn; resume(); unblockingnatural completion → FINISHED; kill()KILLED; blocking call → WAITING; suspend()SUSPENDED
WAITINGBlocked on #delay, @event, mailbox.get(), semaphore.get(), etc.blocking call from RUNNINGunblock → RUNNING; kill()KILLED; suspend()SUSPENDED
SUSPENDEDExplicitly paused by suspend() — state preserved, not blockedsuspend() from RUNNING/WAITINGresume() → back to prior state; kill()KILLED
KILLEDTerminated by kill() — handle still valid, thread deadkill() from any non-terminal state(terminal — no exits)
FINISHEDRan to completion — all statements executednatural end of thread body(terminal — no exits)

Two states are terminal (KILLED, FINISHED); three are transient (RUNNING, WAITING, SUSPENDED). The handle persists after the thread reaches a terminal state — status() continues to work, but the thread's local variables and execution context are gone.

4. Syntax & Semantics — process::self() and the Six Methods

4.1 Getting a handle — process::self()

SystemVerilog — process::self() is the only way to get a handle
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Pattern: thread captures its own handle as the FIRST statement
process driver_proc;       // shared variable for other threads to use
 
initial fork
 
    // ── The driver thread registers itself ────────────────────
    begin : driver_thread
        driver_proc = process::self();   // FIRST line — before any delay or wait
        forever drive_next_txn();
    end
 
    // ── The controller can now act on the driver by handle ────
    begin : controller
        #1000;
        if (driver_proc != null)
            driver_proc.kill();   // kills ONLY the driver, nothing else
    end
 
join_none

The "register your handle as the first statement" discipline is non-negotiable — see DebugLab #1. If the thread does any delay or wait before calling process::self(), the controller may try to use a null handle and crash.

4.2 The complete method set

MethodCalled byWhat it doesBlocks?
process::self()the thread itselfReturns the calling thread's handleNo
p.status()any threadReturns the process::state enum valueNo
p.kill()any thread (or p itself)Terminates p immediately. State → KILLED.No
p.await()any thread except pBlocks the caller until p reaches FINISHED or KILLEDYes
p.suspend()any thread (or p itself)Pauses p. State → SUSPENDED. State preserved.Yes, if called on self
p.resume()any thread except pResumes a SUSPENDED p. State → RUNNING or WAITING.No
SystemVerilog — all six process methods
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── process::self() — get handle from inside the thread ───────
process h;
initial begin
    h = process::self();        // h refers to THIS initial block's process
end
 
// ── p.status() — non-blocking read of current state ───────────
$display("%s", h.status().name());   // "RUNNING", "WAITING", etc.
 
// ── p.kill() — terminate the process immediately ──────────────
h.kill();
// state → KILLED; the rest of h's body is never executed
// the caller is NOT killed — only h
 
// ── p.await() — wait for a specific process to end ────────────
fork
    begin
        h = process::self();
        repeat(50) @(posedge clk);
        $display("Thread done");
    end
join_none
 
h.await();   // caller blocks here until h reaches FINISHED or KILLED
$display("h has finished — safe to proceed");
 
// ── p.suspend() — pause the thread at its current point ───────
h.suspend();   // h is now frozen — it will not execute until resumed
 
// ── p.resume() — restart a suspended thread ───────────────────
h.resume();    // h resumes from exactly where it was paused

4.3 Reading state

SystemVerilog — process::state enum usage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
case (p.status())
    process::RUNNING   : $display("Thread is running");
    process::WAITING   : $display("Thread is blocked");
    process::SUSPENDED : $display("Thread is suspended");
    process::KILLED    : $display("Thread was killed");
    process::FINISHED  : $display("Thread completed");
endcase
 
// ── Convenience predicate ─────────────────────────────────────
function bit is_alive(process p);
    return (p != null &&
            p.status() != process::KILLED &&
            p.status() != process::FINISHED);
endfunction

5. Simulation View — State Transitions in Detail

The simulator drives state transitions in response to scheduler events and explicit method calls:

  • Spawn → RUNNING when the spawning fork schedules the thread for the current Active region.
  • RUNNINGWAITING on the first blocking call (#, @event, mailbox.get(), semaphore.get(), wait, wait fork).
  • WAITINGRUNNING when the blocking condition resolves (event fires, delay expires, mailbox has an item, semaphore has a key).
  • Any non-terminal → SUSPENDED on p.suspend(). The thread is removed from the active scheduler but its program counter, local variables, and pending events are preserved. suspend() called on self blocks the caller until someone else calls p.resume().
  • SUSPENDEDRUNNING or WAITING on p.resume(). The thread re-enters whichever state it was in before suspend().
  • Any non-terminal → KILLED on p.kill(). State is destroyed; the thread cannot be restarted.
  • RUNNINGFINISHED when the thread's body reaches its natural end.

p.await() blocks the caller until p's state reaches a terminal value (KILLED or FINISHED). If p is already in a terminal state when await() is called, the caller returns immediately. Never call p.await() on p itself — the thread cannot finish while it's blocking on itself, producing a deterministic deadlock.

process::self() itself does not change state — it's a pure read of the current process identity. Safe to call multiple times; always returns the same handle within a thread.

6. Waveform — suspend() / resume() Across Reset

The waveform below shows the canonical reset-isolation pattern: a driver thread runs concurrently with a reset manager. When rst_n deasserts at cycle 3, the manager calls drv_proc.suspend(); the driver freezes mid-cycle. When rst_n reasserts at cycle 7, the manager calls drv_proc.resume(); the driver continues from exactly where it left off. The monitor thread runs uninterrupted throughout — suspend() targeted only the driver's handle.

Figure — process.suspend() / resume() pause a driver across reset; monitor untouched

12 cycles
Figure — process.suspend() / resume() pause a driver across reset; monitor untouchedrst_n low → drv_proc.suspend()rst_n low → drv_proc.s…rst_n high → drv_proc.resume()rst_n high → drv_proc.…clkrst_ndriverRUNRUNRUNSUSPSUSPSUSPSUSPRESUMERUNRUNRUNRUNmonitorRUNRUNRUNRUNRUNRUNRUNRUNRUNRUNRUNRUNt0t1t2t3t4t5t6t7t8t9t10t11
rst_n deasserts at cycle 3 → reset manager calls drv_proc.suspend() → driver freezes at its current state (RUNNING → SUSPENDED). rst_n reasserts at cycle 7 → manager calls drv_proc.resume() → driver continues from the exact paused point. The monitor row stays RUN throughout — only the targeted driver handle was suspended, not the whole fork.

This is impossible with disable fork — disable cannot pause, only terminate. The process handle is the only mechanism for the pause-and-resume pattern.

7. Synthesis — Not Applicable

The process class is a procedural simulation construct. It has no hardware footprint and no place in synthesisable RTL — synthesis tools reject it outright. This section is intentionally omitted; the topic does not warrant it.

The corresponding hardware idiom for "pause this activity then resume it" is a registered enable signal (always_ff @(posedge clk) if (en) ...). That is RTL semantics, not a procedural primitive.

8. Verification View — The Canonical Patterns

8.1 The per-agent process-control class

The standard pattern for production verification: every agent captures its own thread handles in start() and exposes pause_drive(), resume_drive(), stop(), and is_alive() on those handles. The agent's lifecycle is fully addressable, and operations on one agent don't touch any other.

SystemVerilog — agent class with built-in per-thread process control
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class ApbAgent;
    process  drv_proc;
    process  mon_proc;
    string   name;
 
    function new(string n); name = n; endfunction
 
    task start();
        fork
            begin
                drv_proc = process::self();          // register FIRST
                $display("[%s] Driver started", name);
                forever drive_next_txn();
            end
            begin
                mon_proc = process::self();
                $display("[%s] Monitor started", name);
                forever sample_response();
            end
        join_none
    endtask
 
    // Pause stimulus without touching the monitor
    task pause_drive();
        if (drv_proc != null) drv_proc.suspend();
    endtask
 
    task resume_drive();
        if (drv_proc != null && drv_proc.status() == process::SUSPENDED)
            drv_proc.resume();
    endtask
 
    task stop();
        if (drv_proc != null) drv_proc.kill();
        if (mon_proc != null) mon_proc.kill();
    endtask
 
    function bit is_alive();
        return (drv_proc != null &&
                drv_proc.status() != process::KILLED &&
                drv_proc.status() != process::FINISHED);
    endfunction
endclass
 
// ── Usage ─────────────────────────────────────────────────────
ApbAgent agent = new("APB_0");
agent.start();
 
// Pause driver during reset, keep monitor alive
@(negedge rst_n);  agent.pause_drive();
@(posedge rst_n);  agent.resume_drive();
 
// Clean stop at end of test
agent.stop();

Each agent owns its handles; methods are surgical. Compare to the disable <block_name> agent in 14.4 §8.3 — handles support suspend()/resume() and status() queries that named-block disable does not.

8.2 The dynamic process pool

When the number of threads is determined at runtime, a process array or queue lets the testbench inspect each thread individually — kill only the ones still waiting, await specific completions, prune finished handles.

SystemVerilog — process pool: track, inspect, selectively kill
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
process pool[$];
 
task automatic spawn_worker(int id);
    fork
        begin
            automatic process me = process::self();
            pool.push_back(me);              // register in the pool
            do_work(id);
            // thread exits naturally — status → FINISHED
        end
    join_none
endtask
 
// ── Spawn 8 workers ───────────────────────────────────────────
for (int i = 0; i < 8; i++) begin
    automatic int id = i;
    spawn_worker(id);
end
 
// ── Print status of every thread in the pool ──────────────────
foreach (pool[i])
    $display("  Worker[%0d] state = %s", i, pool[i].status().name());
 
// ── Kill only threads that are still WAITING (not yet running) ─
foreach (pool[i]) begin
    if (pool[i].status() == process::WAITING) begin
        pool[i].kill();
        $display("  Worker[%0d] killed (was WAITING)", i);
    end
end
 
// ── Purge finished/killed handles from the pool ───────────────
process live[$];
foreach (pool[i]) begin
    if (pool[i].status() == process::RUNNING ||
        pool[i].status() == process::WAITING ||
        pool[i].status() == process::SUSPENDED)
        live.push_back(pool[i]);
end
pool = live;

disable fork cannot do any of this — it terminates by scope, not by selection. The process pool is what makes runtime-policy decisions ("kill only the slow ones") expressible.

8.3 The health-monitor pattern

Background thread that polls every agent's is_alive() status every N cycles and $errors if any agent unexpectedly dies.

SystemVerilog — health monitor: poll every agent's status periodically
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ApbAgent agents[4];
 
task automatic health_monitor();
    forever begin
        repeat(100) @(posedge clk);    // check every 100 cycles
        foreach (agents[i]) begin
            if (!agents[i].is_alive()) begin
                $error("[HEALTH] Agent %0d unexpectedly dead at %0t!", i, $time);
            end
        end
    end
endtask
 
initial fork
    health_monitor();
join_none

This catches "agent silently died mid-test" failures that would otherwise only surface as "scoreboard never sees the expected transactions" much later.

8.4 await() for selective synchronisation

Waiting for one specific thread without waiting for all in-scope threads:

SystemVerilog — await one specific initialiser without waiting for monitors
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
process initialiser_proc;
 
initial fork
    forever monitor_apb();             // background — should NOT be waited on
    begin
        initialiser_proc = process::self();
        load_firmware();
        configure_dut();
        $display("[INIT] Done");
    end
join_none
 
// Main test waits for the initialiser specifically — monitor keeps running
repeat(2) @(posedge clk);              // give it time to register the handle
initialiser_proc.await();              // wait ONLY for the initialiser
$display("[MAIN] Init complete — starting test");
run_test();

wait fork here would block forever because the forever monitor_apb() is also in scope. await() targets exactly one thread — monitors are unaffected.

9. Industry Usage — Where the process Class Lands in Real Verification

  • UVM uvm_sequence::kill() — internally captures the sequence body's process handle when the sequence starts, then kill()s that handle when the user calls seq.kill(). The mechanism is exactly the process::self() + handle-stored-as-class-property pattern.
  • UVM uvm_objection drain mechanics — the phase machinery captures process handles for every component's run_phase() body; phase end uses these handles to detect when each component has reached a terminal state. The progress-callback API exposes this through phase.phase_done.set_drain_time() and the underlying objection counts.
  • UVM uvm_sequencer::start_phase_sequence() — uses a process handle to manage the per-phase default sequence; kills the handle when the phase ends so the sequence doesn't outlive its phase.
  • VIP per-channel suspend/resume — every protocol VIP (APB / AHB / AXI / DDR / PCIe) exposes a pause() / resume() API on its driver agents for reset-isolation tests. The implementation is exactly §8.1's pattern: process::self() captured in start(), drv_proc.suspend() / drv_proc.resume() in the API.
  • Power-aware verification (UPF / CPF) — when a power domain enters retention or shutdown mode, the VIP for that domain must pause its drivers (state preserved, will resume at power-up). disable cannot express this; process.suspend() is the only mechanism.
  • DFT scan mode tests — switching between functional and scan mode requires pausing every functional-mode agent while scan runs. The standard pattern uses a process pool of all agents' handles, iterates with suspend() at scan-mode entry and resume() at scan-mode exit.
  • Health-check daemons in long regressions — the §8.3 pattern wrapped into a reusable UvmHealthMonitor extension that runs across the entire UVM phase tree, catching silent agent death the moment it happens.

10. Design Review Notes — What a Senior Will Flag

Pattern in the diffWhat review will say
process p; #10; p = process::self();"Race — the controller may try to use p before this thread registers. Call process::self() as the first statement of the thread, before any delay, event wait, or blocking call."
me.await(); where me = process::self()"Deadlock — you're waiting for yourself to finish. me cannot reach FINISHED while await() is blocking it. await() is for waiting on other threads."
if (p.status() == process::RUNNING) p.resume();"Wrong predicate — resume() is for SUSPENDED processes only. resume() on a RUNNING process is undefined behaviour. Check status() == SUSPENDED."
p.kill(); p.kill(); (double kill)"Tool-dependent — some simulators no-op, others raise an error. Guard with if (p.status() != KILLED && p.status() != FINISHED) p.kill();."
process driver_proc; declared globally but registered inside a class method"Document the lifecycle. Who clears driver_proc when the agent is destroyed? If a new agent reuses the variable, the old handle is leaked and the new value is stale."
Anonymous fork block with no process::self() capture and a comment "we may need to kill this later""Capture the handle now. Adding it after the fact requires re-running the spawn. Cost: one line. Benefit: future-you can kill it precisely."
Class with process properties but no null check in methods using them"if (drv_proc != null) before every method call. If start() was never called or the agent was constructed without spawning, the handle is null and the method call crashes."
disable fork; in a context where one specific thread should die"Use p.kill() instead — surgical. disable fork kills all children of the calling process; you'll silently take out monitors and clocks."
wait fork; when waiting for a specific thread"Use p.await() instead — targets exactly the thread you mean. wait fork blocks on every in-scope thread; one forever and you deadlock."
process pool[$] that grows monotonically without pruning"Pool leaks finished handles. Add a periodic prune (§8.2) — keep only RUNNING/WAITING/SUSPENDED entries. After 10,000 spawns the pool has 10,000 dead handles all reporting FINISHED on every iteration."

The single highest-value rule: always register process::self() as the first statement of the thread, and always null-check before calling methods on a stored handle. Those two disciplines prevent almost every process-class bug.

11. Debugging Guide — Real Failures, Real Fixes

1

Controller crashes with 'null handle' before doing anything

SELF-CALLED-TOO-LATE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
process p;
initial fork
    begin
        #10;                         // BUG: thread runs for 10 before registering
        p = process::self();
    end
join_none
 
#5;                                  // controller runs at t=5
p.kill();                            // BUG: p is still null — crash
Symptom
Simulation aborts with "null handle method call." Stack trace points at the p.kill() line. The thread it was supposed to kill hadn't even started yet. Reproduces 100% of the time at the same seed.
Root Cause
The spawned thread does #10 before calling process::self(). The controller dereferences p at t=5, before the thread has registered its handle. p is still null.
Fix
process::self() must be the first statement of the thread, before any delay, event wait, or blocking call. Then the handle is set the moment the thread is spawned, and any controller can dereference it safely (with a null check for robustness).
2

Init task hangs forever; await() never returns

AWAIT-SELF-DEADLOCK
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task automatic init_with_await();
    process me = process::self();
    do_init_work();
    me.await();                      // BUG: waiting for myself to finish
    // Unreachable — me can't FINISH while await() is blocking it
endtask
Symptom
init_with_await() runs do_init_work() then hangs. Watchdog eventually fires. Log shows the task entered but never exited.
Root Cause
p.await() blocks the caller until p reaches FINISHED or KILLED. When the caller is p, the caller is blocked on itself — it can never reach FINISHED because the body that would reach endtask is the one blocked.
Fix
await() is for waiting on other threads. Remove the call (the task naturally finishes when its body ends) or move the wait to a different process that wants to know when this one is done.
3

resume() silently does nothing; driver stays paused

RESUME-WRONG-STATE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task resume_driver();
    if (drv_proc != null) drv_proc.resume();   // no status guard
endtask
 
// Caller flow:
drv_proc.suspend();
resume_driver();
resume_driver();                     // BUG: second call resumes a non-SUSPENDED process
// Driver may still be paused — behaviour tool-dependent
Symptom
Driver appears to be stuck after the second call. Test stalls. Verdi shows drv_proc.status() = SUSPENDED even though resume() was called twice.
Root Cause
resume() on a non-SUSPENDED process is undefined behaviour per IEEE 1800. Some simulators silently no-op; others raise an error. The double-resume here was meant to be idempotent but is actually a contract violation.
Fix
Always guard resume() with a status check: if (drv_proc.status() == process::SUSPENDED) drv_proc.resume();. The same discipline applies to suspend() (don't suspend KILLED/FINISHED) and kill() (don't kill KILLED/FINISHED).
4

Health monitor reports 'agent dead' even after agent.start()

HANDLE-NEVER-CAPTURED
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class ApbAgent;
    process drv_proc;
    task start();
        fork
            begin
                // BUG: forgot process::self()
                forever drive_next_txn();
            end
        join_none
    endtask
 
    function bit is_alive();
        return (drv_proc != null &&
                drv_proc.status() != process::KILLED &&
                drv_proc.status() != process::FINISHED);
    endfunction
endclass
 
// Health monitor:
if (!agent.is_alive()) $error("Agent dead!");   // fires immediately
Symptom
Health monitor reports [HEALTH] Agent 0 dead at t=100 even though the agent's driver is clearly running and producing log output. Test passes the actual functional checks but the health monitor spams errors.
Root Cause
drv_proc was declared but never assigned — process::self() was forgotten inside the spawned thread. The handle stays null. is_alive() returns false (the null check is the first conjunct). The driver is actually running, but the agent has no handle to it.
Fix
Add drv_proc = process::self(); as the first statement of the forked block, before the forever. Lint rules at every shop flag process declarations that are never assigned via self().
5

Process pool grows without bound; performance degrades over time

POOL-NEVER-PRUNED
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
process pool[$];
 
task automatic spawn_worker(int id);
    fork
        begin
            automatic process me = process::self();
            pool.push_back(me);
            do_work(id);
        end
    join_none
endtask
 
// 10,000 spawns over the test...
foreach (pool[i]) check_status(pool[i]);   // O(N) every iteration!
Symptom
Test runs progressively slower. Profiler shows check_status iteration time growing linearly. After 1 hour, simulator is spending 50% of CPU walking the pool. Functional behaviour correct but throughput unacceptable.
Root Cause
Pool grows monotonically — every spawned thread is added; nothing is ever removed. After N spawns the pool has N entries, most of them FINISHED. Every iteration over the pool is O(N), and N grows linearly with simulation time.
Fix
Periodically prune dead handles (§8.2): iterate the pool, copy live entries (RUNNING/WAITING/SUSPENDED) to a new queue, reassign. Run the prune every M spawns or every K cycles. Pool size stabilises at the steady-state count of live threads.

12. Interview Insights — What Interviewers Actually Probe

process is a built-in SystemVerilog class. Every running thread has exactly one associated process object. You cannot construct one with new() — the only way to obtain a handle is to call process::self() from inside the thread whose handle you want. The thread typically captures its own handle as the first statement of its body and stores it in a shared variable so other threads can act on it.

13. Exercises

1. Design — reset-isolated driver (Foundation)
Build an ApbAgent class that captures its driver thread's process handle via process::self(), and exposes pause_during_reset() and resume_after_reset() methods. Confirm the monitor thread is untouched by the pause/resume cycle.

2. Debug — the silent agent (Intermediate)
A teammate's ApbAgent reports is_alive() == false from the health monitor, but the driver is clearly producing log output. The agent class has process drv_proc; declared and the driver thread runs forever drive_next_txn();. Identify the missing line and write the fix.

3. Code review — the resume bug (Intermediate)
A teammate's resume() method is if (drv_proc != null) drv_proc.resume();. The driver gets stuck after a sequence of operations. Identify why this implementation is unsafe and propose the canonical fix with a status guard.

4. Trade-off — process pool vs disable fork (Advanced)
A teammate argues: "process pools are overkill — disable fork is simpler and shorter." Argue the case against the blanket rule. Construct a real verification scenario (multi-agent regression with per-agent failure injection) where the process pool is required and disable fork cannot express the needed semantics.

14. Summary

The process class is the precision counterpart to disable fork. Every running thread has one process object; the thread captures its handle via process::self() (always the first statement of the body); other code uses the handle to kill() / suspend() / resume() / await() that exact thread. Five states track the lifecycle (RUNNING, WAITING, SUSPENDED, KILLED, FINISHED); six methods drive the transitions.

Defaults to memorise. process::self() is always the first line. Always null-check before dereferencing a stored handle. Use p.kill() for surgical termination, disable fork for scope-wide cleanup. suspend()/resume() is the only way to pause-and-restart a thread with state preserved. Never await() on self — instant deadlock. Periodically prune long-lived process pools to avoid O(N) iteration growth.


Module 14 complete. The five lessons together cover every fork-lifecycle primitive SystemVerilog provides:

  • 14.1 fork-join — spawn parallel threads and block until all complete. Loop-variable capture is the canonical first-fork bug.
  • 14.2 fork-join_any — block until any one thread completes. The timeout idiom. Always pair with disable fork.
  • 14.3 fork-join_none — don't block at all. The basis for daemons and variable-N spawning. Pair with wait fork for finite-thread barriers.
  • 14.4 disable fork & wait fork — scope-wide kill and barrier. Always wrap in a task automatic to isolate scope.
  • 14.5 process Class — handle-based precision. The only way to target one specific thread, pause/resume, or query status.

Pick by intent: disable fork for scope; disable <block_name> for named-thread surgical kill; p.kill() for runtime-targeted surgical kill. The same trichotomy applies to waiting — wait fork for scope, await(named_block.triggered) for named blocks, p.await() for handles.

Next module: 15 — Program Blocks (begins with 15.1 Program Block vs Module — Race-Free Simulation).