Fork, join and cancellation
Predict when a parent process continues after join, join_any and join_none, and stop or wait for exactly the child processes you mean.
- Lesson
- 3.3
- Time
- 25 min
- Runs on
- Verilator
- Check
- 5 questions
A read test fails with TIMEOUT: no response to read 0x40 at 1,000 ns, yet the waveform shows the response at 100 ns and the scoreboard matched it. The read task raced the response against a 1,000 ns timer with fork ... join_any and returned as soon as the response won. Nobody stopped the timer, so at 1,000 ns the forgotten branch woke up and printed its error.
One column per 100 ns. The task returned at 100 ns, but its timer branch kept running.
Three ways to finish a fork
A fork starts each statement inside it as a separate child process, and all of them begin in the same time step. Wrap several statements in begin ... end to make them one child, so fork begin a; b; end c; join has two children.
The keyword that closes the fork decides when the parent, the process that ran the fork, continues. It does not decide anything about the children.
| Closing keyword | The parent continues when | Children still running afterwards |
|---|---|---|
join | every child has finished | none |
join_any | the first child finishes | all the others, untouched |
join_none | at once, before any child starts | all of them |
The parent and its children after each kind of fork.
Each form has a typical use. join runs a fixed set of activities side by side, such as a write sequence and a read sequence that must both finish. join_any races alternatives, such as a response against a timer. join_none launches long-lived processes, such as monitors, and lets the parent carry on.
One column per time unit. The children run the same way in all three cases; only the parent differs.
With join_none, the children do not even start at the fork. They start once the parent blocks or ends, so the statements right after join_none always run first.
The parent reaches the fork
It creates the children. They are ready to run, but none has started.
The parent carries on
It runs the statements after
join_nonewithout stopping, in the same time step.The parent blocks
At its next delay, event control or
wait fork, the children start running.
The losing branch keeps running
A timeout race needs join_any, because the task must return as soon as either branch finishes. The branch that lost is still a live process, though. In the opening story the timer was still counting, and it fired 900 ns after the test had moved on.
A false error like this is expensive. It names a read that actually passed, so someone usually spends an afternoon debugging the DUT before finding the bug in the testbench.
The statement disable fork stops the children that are still running. Put it right after join_any, and whichever branch lost is stopped before it can do anything.
join_any alone
fork
wait (rsp);
begin #1000 $error("TIMEOUT"); end
join_any
Returns when the response arrives and leaves the timer running, so the error fires later.
join_any, then disable fork
fork
wait (rsp);
begin #1000 $error("TIMEOUT"); end
join_any
disable fork;
Stops the timer as soon as the response wins.
One case stays open. If the response and the timer finish in the same time step, the standard does not say which branch runs first, so the outcome can differ between simulators. Avoid the tie by checking the response again after join_any, or by counting clock edges as lesson 3.7 does.
disable fork stops every child of the process
disable fork terminates every active descendant of the process that runs it: its children, their children, and so on. The process is the unit that matters here, not the fork statement above it.
A task call does not create a new process, because the task runs inside its caller. So a disable fork inside a task also kills children that the caller started earlier, such as a background monitor forked at the start of the test.
Bare disable fork in a task
task wait_rsp();
fork
wait (rsp);
#20 timed_out = 1;
join_any
disable fork;
endtask
Stops the timer and also every earlier child of the caller, including its monitor.
Wrapped in its own process
task wait_rsp();
fork begin
fork
wait (rsp);
#20 timed_out = 1;
join_any
disable fork;
end join
endtask
The outer fork begin ... end join makes a new process, so disable fork stops only the race.
The wrapper is cheap, and it makes the task safe to call from anywhere. Use it every time you write disable fork in a task or a class method.
Waiting for children with wait fork
wait fork blocks until every immediate child of the process has finished. It counts children started by any earlier fork, not only the most recent one.
Its typical use is at the end of a sequence that launched several transfers with join_none. The sequence must not report that it is done until every transfer has finished, and wait fork is the line that waits for them.
If one of those children is a forever monitor, wait fork never returns. Wrap the forks you want to wait for in their own process, just as you do for disable fork.
Try it
Part 1 shows the table at work. join_none returned prints before C runs, and wait fork also waits for B, a child of the earlier join_any.
In part 2, the first monitor starts at t=5 and beats once, at t=9. The bare disable fork kills it at t=11, so its count is stuck at 1. The second monitor survives the wrapped race and beats at t=26, 30, 34 and 38. Delete the rsp = 1 lines and predict what changes.
t=2 A done t=2 join_any returned t=2 join_none returned t=3 C done t=5 B done t=5 wait fork returned t=11 response t=22 bare: beats=1 t=28 response t=39 wrapped: beats=4Check yourself
Remember
joinwaits for every child,join_anyfor the first, andjoin_nonefor none; no join stops a child.- Children of
join_nonestart only when the parent blocks or ends. - After
join_any, stop the losing branch withdisable fork, or it keeps running. disable forkandwait forkact on every child of the process, so wrap a race infork begin ... end join.- When two branches finish in the same time step, which one wins is unspecified, so never depend on it.
Practice
- Wait for two workers Implement · Easy
- Cancellation scope repair Fix · Medium
- The timer that outlived its race Debug · Medium