Chapter 14 of 16
Processes and Interprocess Communication: fork-join, Events, Semaphores, and Mailboxes
Launch concurrent processes explicitly with the three fork-join variants, clean up background processes with disable fork/wait fork, then coordinate them with event, semaphore, and mailbox — synchronizing, limiting resource access, and passing data between processes, the concurrency foundation real testbenches are built on.
Chapter 9 used fork...join to demonstrate the automatic lifetime gotcha, without formally covering it. A real testbench usually has several concurrent parts running at once — generating stimulus, driving signals, monitoring outputs, a scoreboard comparing results — this chapter properly covers how to explicitly launch these concurrent processes, and how they coordinate with each other.
fork...join: explicitly launching concurrent processes
Every initial/always block already runs concurrently with the others; fork...join launches additional concurrent processes from inside a single procedural block:
initial begin
fork
$display("process A at time %0t", $time);
$display("process B at time %0t", $time);
join
$display("both done");
endfork...join comes in three variants, differing in when the surrounding code resumes:
| Variant | Behavior |
|---|---|
join | Waits for every forked process to finish before continuing |
join_any | Continues as soon as any one forked process finishes (the rest keep running in the background) |
join_none | Doesn't wait at all — continues immediately (every forked process runs in the background) |
initial begin
fork
begin #10; $display("A done at %0t", $time); end
begin #20; $display("B done at %0t", $time); end
join_any
$display("continuing at %0t (B might still be running)", $time);
endIn the example above, join_any lets the surrounding code continue as soon as A finishes (after 10 time units), while B is still running and continues in the background until it finishes at time unit 20.
disable fork and wait fork: cleaning up processes left running in the background
After join_any/join_none, some forked processes can be left running in the background — in the example above, B keeps running until time unit 20 even after join_any moves on. Left alone, that process runs to its natural end, which can cause two kinds of trouble: it might keep consuming resources or mutating shared state after you've stopped caring about it, or you might actually want to wait for it to fully finish too. Two built-in constructs exist specifically for cleaning up processes still running in the background:
initial begin
fork
begin #10; $display("A done at %0t", $time); end
begin #20; $display("B done at %0t", $time); end
join_any
// A has finished here; B is still running in the background
disable fork; // immediately terminates every still-running child process this process forked (B, here)
$display("B has been killed, continuing at %0t", $time);
enddisable fork: immediately terminates every child process the current process launched viaforkthat hasn't finished yet — a common case isjoin_anymoving on once the first process wins, with the still-running loser no longer needed and killed outright to stop it from consuming resources or causing unwanted side effects.wait fork: terminates nothing; instead it blocks until every child process the current process forked has finished (including ones still running in the background afterjoin_any/join_none) — for when you genuinely need to wait for all of them, just not by blocking right at thejoinline.
The two are opposites: disable fork means "stop waiting, kill it now"; wait fork means "still waiting, just somewhere else."
event: a synchronization signal between processes
An event is a lightweight synchronization signal that carries no data of its own — purely a way to coordinate "has this happened yet": one process triggers it, another waits for it to be triggered:
event data_ready;
initial begin
#10;
-> data_ready; // trigger the event
end
initial begin
@(data_ready); // block until the event is triggered
$display("data is ready at time %0t", $time);
endThe second initial block blocks at @(data_ready); until the first block triggers the event at time 10, at which point it continues.
semaphore: managing a limited pool of shared resources
A semaphore maintains a count of "keys": a process calls get(n) to try to take n keys, blocking if there aren't enough available; once done, it calls put(n) to return them. The most common use sets the key count to 1, implementing mutual exclusion (similar to a lock in other languages):
semaphore sem = new(1); // only 1 key — equivalent to a mutex
task automatic access_resource(int id);
sem.get(1);
$display("process %0d entering critical section at %0t", id, $time);
#5;
$display("process %0d leaving critical section at %0t", id, $time);
sem.put(1);
endtask
initial begin
fork
access_resource(1);
access_resource(2);
join
endSince there's only 1 key, the two concurrent calls can never both be inside the "critical section" at once: whichever process gets the key first must finish its #5 delay and call put(1) to return it before the other process can acquire the key and continue — the entering/leaving prints from the two processes never interleave.
mailbox: passing data between processes
An event can only synchronize — it carries no data. A mailbox is a first-in-first-out queue built specifically for passing data between processes: one process put()s data in, another get()s it out:
mailbox #(int) mbx = new(); // #(int) means this mailbox only carries int data
initial begin // producer
for (int i = 0; i < 3; i++) begin
mbx.put(i);
$display("produced %0d", i);
end
end
initial begin // consumer
int val;
repeat (3) begin
mbx.get(val);
$display("consumed %0d", val);
end
endget() blocks until there's data available to retrieve; if the mailbox was created with a capacity limit (e.g. new(4)), put() also blocks once the mailbox is full, until there's room. This is exactly the most common communication pattern between a "stimulus generator" and a "driver" in a classic testbench — the generator put()s transaction objects into the mailbox one by one, and the driver get()s them out and drives them onto the DUT, with the two running as independent concurrent processes that only exchange data through the mailbox.
Summary
| Mechanism | Carries | Typical use |
|---|---|---|
fork...join/join_any/join_none | — | Explicitly launching multiple concurrent processes, choosing how long to wait |
disable fork / wait fork | — | Cleaning up child processes left running in the background after join_any/join_none: disable fork kills them, wait fork blocks until they finish |
event | No data, a pure synchronization signal | Notifying "this has happened" |
semaphore | A count of access permits | Mutual exclusion (key count = 1) or limiting concurrent resource usage |
mailbox | Actual data/objects, first-in-first-out | Passing transactions between concurrent processes, like a stimulus generator and a driver |
What's the main difference between fork ... join_any and fork ... join?
After fork ... join_any, one forked child process is still running in the background. To terminate it immediately instead of waiting for it to finish, which construct should you use?
Wrapping a block of code with semaphore sem = new(1); plus get(1)/put(1) mainly achieves what?
Which method retrieves a piece of data from a mailbox? (lowercase, no parentheses)