Plumbing moves transactions between testbench parts without races or leaks. Containers hold them, threads drive them, and channels synchronize them. Each answer below is the short version with a link to the lesson.
Pattern | Container | Example |
|---|---|---|
Known size, dense indices | Dynamic array | A packet payload allocated once |
Sparse or non-integer keys | Associative array | A memory model keyed by address |
Ordered, grows and shrinks | Queue | Scoreboards, generators, monitors |
Reading a missing associative entry returns the type default without allocating, so guard reads with exists().
Taught in: Dynamic Arrays and Queues, Associative Arrays.
Fixed and dynamic arrays allocate every element. Associative arrays allocate per written element, the only sane way to model a large address space. An unbounded queue that is pushed but never popped is the classic testbench leak, so check queue-empty at end of test and print watermarks on long runs.
Array locator and ordering methods with with replace loops. Locator methods return a queue, possibly empty, never a scalar, so guard max, min and unique results with size() and cast inside sum so a narrow field cannot wrap.
Taught in: Array Methods and Selection.
join waits for all branches, join_any resumes at the first, and join_none resumes immediately for background services. With join_any and join_none the other branches keep running. Children of join_none start only when the parent yields.
Every branch captures the same loop variable, and join_none branches start after the loop ends, so all branches see the exit value. Copy the variable into an automatic local inside the fork. Separately, disable fork kills every child of the current thread, including monitors launched earlier, so wrap the timeout in an isolating fork...join.
The bench proves both fixes: per-branch capture gives 0 1 2 3, and join_any plus disable fork ends the slow branch. (Verilator 5.048 already copies the loop variable per iteration, so the capture half passes there even without the fix; Icarus shows the trap.)
module tb_fork_catch;
timeunit 1ns; timeprecision 1ps;
int errors = 0;
int hit[4];
bit slow_done = 0;
initial begin
for (int i = 0; i < 4; i++) hit[i] = -1;
for (int i = 0; i < 4; i++) begin : gen
automatic int idx = i;
fork begin : worker
#10;
hit[idx] = idx;
end join_none
end
#11;
for (int i = 0; i < 4; i++) if (hit[i] !== i) errors++;
fork
begin #5; end
begin #100; slow_done = 1; end
join_any
disable fork;
#110;
if (slow_done !== 1'b0) errors++;
if (errors == 0) $display("PASS: loop capture gave 0 1 2 3 and disable ended the slow branch");
else $fatal(1, "FAIL: fork behavior wrong");
$finish;
end
endmoduleTaught in: Threads and Control: Fork, Disable, Wait, Process.
A mailbox is a thread-safe FIFO with blocking built in: get suspends while empty and a bounded put suspends while full. A hand-rolled queue plus event races when the trigger lands before the wait. Bound the mailbox so generation stays coupled to consumption.
Taught in: Mailboxes: Typed Pipes with Backpressure.
A semaphore manages keys for a shared resource: one key is a mutex, N keys model limited outstanding transactions. Keys leak when a holder is killed before put, which deadlocks everyone behind it.
Taught in: Semaphores: One Key and Many.
@(ev) only sees a trigger that arrives while already waiting. ev.triggered stays true for the rest of the time step, so wait(ev.triggered) passes whether the process arrived before or after the trigger. Cross-timestep handoffs still belong in a mailbox.
Taught in: Events: Edge Waits and Level Fixes.
The bound adds backpressure: a fast generator cannot race thousands of transactions ahead. Memory stays flat and stimulus keeps responding to DUT state.
disable fork kills it. What does the rest of the test see?A hang. Everyone behind get blocks forever on a key that never returns, which looks like a DUT deadlock.