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.

The losing branch is still alive
012345678910reqrspresponse branchwaitdone-timer branchwaitERRORjoin_any returnsfalse TIMEOUT

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 keywordThe parent continues whenChildren still running afterwards
joinevery child has finishednone
join_anythe first child finishesall the others, untouched
join_noneat once, before any child startsall 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.

Children with delays of 2, 5 and 9
0123456789child A (#2)rundonechild B (#5)rundonechild C (#9)rundoneparent after joinwaitrunparent after join_anywaitrunparent after join_nonerunfirst child donelast child done

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.

  1. The parent reaches the fork

    It creates the children. They are ready to run, but none has started.

  2. The parent carries on

    It runs the statements after join_none without stopping, in the same time step.

  3. 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.

RUNVerilator · simulator service
Edit the code, then run it
Expected: 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=4

Check yourself

Predict the outputPredict the exact output.
module tb;
  int done_count;
  initial begin
    fork
      begin #2 done_count++; end
      begin #5 done_count++; end
      begin #9 done_count++; end
    join_any
    #4 $display("t=%0t done=%0d", $time, done_count);
  end
endmodule
Spot the bugA test starts a background monitor, then calls wait_rsp(). The response arrives at t=6. Each option is a version of wait_rsp; timed_out must stay 0 and the monitor must keep running. Which option breaks the monitor?
ConceptAt time 0 a process runs fork $display("child"); join_none and then $display("parent");. What is printed?
ConceptA test forks a forever coverage monitor at time 0, later forks three sequences with join_none, and then runs wait fork to wait for the sequences. What happens?
Read the waveformA process forked monitor with join_none earlier. It then runs fork A; B; join_any followed by disable fork;. A finishes at t=4. Which processes are still running at t=6?
join_any followed by disable fork
0123456monitor (forked earlier)run?A (#4)rundoneB (#7)run?parentwaitdisable forkrunjoin_any returns

One column per time unit. The question marks are for you to fill in.

Remember

  • join waits for every child, join_any for the first, and join_none for none; no join stops a child.
  • Children of join_none start only when the parent blocks or ends.
  • After join_any, stop the losing branch with disable fork, or it keeps running.
  • disable fork and wait fork act on every child of the process, so wrap a race in fork begin ... end join.
  • When two branches finish in the same time step, which one wins is unspecified, so never depend on it.

Practice