← Design Verification Interviews

Interview Question · Code Reading

Join, Join Any and Join None

L0 FresherL1 Juniorsystemverilogprocessesfork join
  • Asked at Google

The Question

The interviewer asks

When does the parent resume after join, join_any and join_none? Predict the output of this block.

systemverilog
initial begin
  fork
    #1 $display("t=%0t fast child done", $time);
    begin #3 late = 1; $display("t=%0t slow child done", $time); end
  join_any
  $display("t=%0t parent resumes", $time);
  #4 $display("late=%0d", late);
end
Reveal the Model Answer

join resumes when every child has finished, join_any when the first one does, and join_none at once without waiting. Here the parent resumes at t=1 with the fast child, the slow child still finishes at t=3, and late=1 prints at t=5. join_any stops the waiting, not the other children.

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
  1. Cycle 0

    The fork creates children A, B and C. After join_none the parent goes on at once; after join or join_any it waits.

  2. Cycle 2

    A finishes first, so join_any releases its parent. B and C keep running.

  3. Cycle 5

    B finishes. The parent after join still waits, because C is running.

  4. Cycle 9

    C finishes, the last child, and join releases its parent.

One column per time unit. The children run the same way in all three cases; only the parent differs.

RUNVerilator · simulator service
Edit the code, then run it
Expected: t=1 fast child done t=1 parent resumes t=3 slow child done late=1

Self-Check

Tick each key point you covered in your answer.

What Interviewers Listen For (Fresher)

A good fresher answer states the three rules and draws the timeline. It shows the slow child finishing after the parent has already moved on.

Cross-Questions

Where the interviewer goes next. Open a node to practise that follow-up.

WhyWhat IfProve ItCompareScale It

  1. Join, Join Any and Join None
  2. What IfThe Timeout That Killed the MonitorL1 JuniorL2 Mid-Level

    Asked once they add disable fork

    After a request-with-timeout task was added, a background beat counter in the same test stops counting. Here is the task and the log. What happened?

    log
    // task wait_resp: fork response / timeout join_any; disable fork;
    @0    beat counter forked (join_none)
    @25   beats=3
    @30   response received, disable fork
    @80   beats=3          <- counter stopped at t=30
    Reveal the Model Answer

    disable fork kills every active descendant of the calling process, not just the fork above it. The task runs in the test's process, so it also killed the counter the test had forked earlier. Put the race in its own scope, fork begin ... end join around it, so disable fork only reaches the response and timeout branches.

    Which Children Disable Fork StopsA test runs a response-or-timeout race twice: bare inside a task, then wrapped in its own process. Arrows point from a parent process to the children it forks. A dashed badge is a fork or a disable fork; a solid badge is a child finishing.
    Which children disable fork stopsA test runs a response-or-timeout race twice: bare inside a task, then wrapped in its own process. Arrows point from a parent process to the children it forks. A dashed badge is a fork or a `disable fork`; a solid badge is a child finishing. child Monitor forever #4 beats++ process Test process calls the wait task child Wrapper process fork begin ... end join child Response branch bare race: wait (rsp) child Timer branch bare race: #20TIMEOUT child Response branch wrapped race: wait(rsp) child Timer branch wrapped race: #20TIMEOUT childchildchildchildchildchild
    1. Bare: everything is a child of the testt=5 (illustrative)

      The test forks the monitor. Then wait_rsp_bare runs inside the test process, so the response and timer branches it forks are children of the test too.

      More context

      The response model, fork #6 rsp = 1; join_none, is a child of the test as well. It sets rsp at t=11 and ends, so it is not drawn.

      Follow the calls, values and waits
      1. call fork: Test process → Monitor
      2. call fork: Test process → Response branch
      3. call fork: Test process → Timer branch
      4. Waiting: Test process — join_any
      5. Waiting: Response branch — waiting for rsp
      6. Waiting: Timer branch — counting to t=25
    2. The response winst=11 (illustrative)

      The response branch prints t=11 response and finishes, so join_any returns. The monitor has beaten once, at t=9.

      Follow the calls, values and waits
      1. return done: Response branch → Test process
      2. Released: Response branch
      3. Released: Test process
    3. disable fork in the test processt=11 (illustrative)

      disable fork stops every child of the process that runs it. That is the test, so the timer and the monitor both stop, and t=22 prints beats=1.

      Follow the calls, values and waits
      1. call disable: Test process → Timer branch
      2. call disable: Test process → Monitor
      3. Released: Timer branch
    4. Wrapped: the race gets its own parentt=22 (illustrative)

      The test forks a new monitor. wait_rsp_wrapped forks a wrapper process, and the wrapper forks the response and timer branches.

      Follow the calls, values and waits
      1. call fork: Test process → Monitor
      2. call fork: Test process → Wrapper process
      3. call fork: Wrapper process → Response branch
      4. call fork: Wrapper process → Timer branch
      5. Waiting: Test process — join
      6. Waiting: Wrapper process — join_any
      7. Waiting: Response branch — waiting for rsp
      8. Waiting: Timer branch — counting to t=42
    5. disable fork in the wrappert=28 (illustrative)

      The response wins at t=28. Now disable fork runs in the wrapper, so it stops only the timer. The wrapper ends and the task returns.

      Follow the calls, values and waits
      1. return done: Response branch → Wrapper process
      2. call disable: Wrapper process → Timer branch
      3. Released: Response branch
      4. Released: Timer branch
      5. Released: Wrapper process
      6. Released: Test process
    6. The monitor survivest=39 (illustrative)

      The monitor beats at t=26, 30, 34 and 38, so t=39 prints beats=4.

    Read the diagram as text
    • Monitor: forever #4 beats++
    • Test process: calls the wait task
    • Wrapper process: fork begin ... end join
    • Response branch: bare race: wait (rsp)
    • Timer branch: bare race: #20 TIMEOUT
    • Response branch: wrapped race: wait (rsp)
    • Timer branch: wrapped race: #20 TIMEOUT
    • Test process → Monitor: child
    • Test process → Response branch: child
    • Test process → Timer branch: child
    • Test process → Wrapper process: child
    • Wrapper process → Response branch: child
    • Wrapper process → Timer branch: child

    disable fork acts on the process that runs it. The wrapper gives the race a process of its own.

    Self-Check

    Tick each key point you covered in your answer.

    What Interviewers Listen For (Junior)

    A good junior answer says that calling a task does not create a new process. So disable fork inside the task reaches the caller's children too.

    Misconception

    "disable fork kills only the processes of the fork statement just before it."

    It kills all active descendants of the current process. The twin loses a background monitor forked earlier by the same test.

    Run It

    A response-or-timeout task called from a test that already forked a beat counter.

    RUNVerilator · simulator service
    Edit the code, then run it
    Expected: ok=1 beats=3 FAIL: disable fork also killed the monitor this process forked earlier
    See the Fix
    module tb;
      logic clk = 0;
      int beats = 0;
      bit ok;
      always #5 clk = ~clk;
      task automatic wait_resp();
        fork begin                   // the fix: a private scope for the race
        fork
          begin #30 ok = 1; end      // the response
          begin #100 ok = 0; end     // the timeout
        join_any
        disable fork;                // kill the loser
        end join
      endtask
      initial begin
        fork
          forever @(posedge clk) beats++;   // a background monitor forked earlier
        join_none
        wait_resp();
        #50 $display("ok=%0d beats=%0d", ok, beats);
        if (beats < 5) $display("FAIL: disable fork also killed the monitor this process forked earlier");
        else $display("PASS: only the timeout was killed; the monitor kept counting");
        $finish;
      end
    endmodule
    

    Prints: ok=1 beats=8 PASS: only the timeout was killed; the monitor kept counting

    Missed Corner

    "Use disable wait_resp instead; it only touches the task."

    Disabling a named task stops every active call of that task, in every thread, which is a different trap.

    Source IEEE 1800-2017 §9.6.2

  3. WhyWhy a Task Call Shares the Caller's ProcessL1 JuniorL2 Mid-Level

    Why did the task's disable fork reach a process the task never created?

    Reveal the Model Answer

    Calling a task does not start a new process; the task's statements run in the caller's process, as if pasted in. So the children of "the current process" include everything the caller had forked before the call. Only fork creates new processes.

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "Every task call runs in its own thread."

    Tasks run in the calling thread. Concurrency comes only from fork, initial, always and similar constructs.

    Source IEEE 1800-2017 §9.5

  4. What IfScoping Disable Fork to the TaskL1 JuniorL2 Mid-Level

    What if you must keep disable fork in the task? How do you make it stop only the task's own children?

    Reveal the Model Answer

    Wrap the race in its own process: fork begin ... end join. Inside that block, the race's children are the only children of the new process. A disable fork there stops only them. The caller's monitors keep running.

    Self-Check

    Tick each key point you covered in your answer.

    Missed Corner

    "Use wait fork instead; it is safer."

    wait fork also acts on every child of the process, including the caller's monitors. A forever monitor makes it hang.

    Source Lesson 3.3 key points

  5. CompareProcess Handles vs Disable ForkL2 Mid-Level

    Compare killing the timeout through a process handle with using disable fork.

    Reveal the Model Answer

    Inside each branch, process::self() returns a handle, and calling kill() on the timeout's handle stops exactly that process and its descendants. disable fork is shorter but reaches everything the current process has forked. Handles also let you await() a process or check its status(), which suits library code that must not touch the caller's threads.

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "kill() on a handle stops only that process; its children keep running."

    Killing a process also kills the processes it spawned.

    Source IEEE 1800-2017 §9.7

  6. Prove ItSolve 3.3.3: Cancellation Scope RepairSimulator-graded

    Show it, don't just say it: this Silicon Lab problem is graded by the simulator.

    Open Problem 3.3.3

  7. Scale ItProcesses That Outlive Their TestL3 Senior

    A regression runs hundreds of sequences in one simulation. How do leftover timers and watchers from join_any races show up, and what rule prevents them?

    Reveal the Model Answer

    They fire later, in an unrelated sequence. A stale timeout reports an error, or a watcher takes an event meant for someone else. The failure depends on which sequences ran before. The rule is that every race ends with an explicit cleanup in its own scope, fork begin ... end join around the race plus disable fork. End-of-test checks then confirm that no sequence-owned processes are still alive.

    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.

    Self-Check

    Tick each key point you covered in your answer.

    What Interviewers Listen For (Senior)

    A good senior answer links failures that depend on test order to processes left running. It turns the cleanup into a review rule and an end-of-test check.

    Misconception

    "Leftover processes die when the sequence object is garbage collected."

    A process is not owned by the object that started it. It runs until it finishes or is killed.

    Source IEEE 1800-2017 §9.7

  8. CompareFork Join vs Begin EndL0 Fresher

    Compare begin #5 a = 1; #5 b = 1; end with the same two statements inside fork ... join. When does b become 1?

    Reveal the Model Answer

    In begin ... end the statements run in sequence, so b is set at t=10. In fork ... join, each statement is its own process, and all start at the same time. Both delays count from t=0, so b is set at t=5. The fork still finishes only when both are done.

    Self-Check

    Tick each key point you covered in your answer.

    Half-Truth

    "fork runs statements in parallel, so it is always faster."

    It changes when they start, not how long each takes. Dependent steps inside a fork can race or run in the wrong order.

    Source IEEE 1800-2017 §9.3.2

  9. Prove ItSolve 3.3.1: Wait for Two WorkersSimulator-graded

    Show it, don't just say it: this Silicon Lab problem is graded by the simulator.

    Open Problem 3.3.1

  10. Scale ItProcesses That Outlive Their TestL3 Senior

    A regression runs hundreds of sequences in one simulation. How do leftover timers and watchers from join_any races show up, and what rule prevents them?

    Reveal the Model Answer

    They fire later, in an unrelated sequence. A stale timeout reports an error, or a watcher takes an event meant for someone else. The failure depends on which sequences ran before. The rule is that every race ends with an explicit cleanup in its own scope, fork begin ... end join around the race plus disable fork. End-of-test checks then confirm that no sequence-owned processes are still alive.

    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.

    Self-Check

    Tick each key point you covered in your answer.

    What Interviewers Listen For (Senior)

    A good senior answer links failures that depend on test order to processes left running. It turns the cleanup into a review rule and an end-of-test check.

    Misconception

    "Leftover processes die when the sequence object is garbage collected."

    A process is not owned by the object that started it. It runs until it finishes or is killed.

    Source IEEE 1800-2017 §9.7

What People Get Wrong

Common wrong answers and why they fail. Run It shows the wrong belief failing on a real simulator.

Misconception

"join_any kills the children that did not finish."

The other children keep running. In the twin a timeout left behind fires after the response has already won.

Run It

A response-or-timeout race with join_any and no cleanup.

RUNVerilator · simulator service
Edit the code, then run it
Expected: t=2 response arrived timer_fired=1 FAIL: join_any only stops waiting; the timer kept running and fired after the response
See the Fix
module tb;
  bit timer_fired = 0;
  initial begin
    fork
      #2 $display("t=%0t response arrived", $time);
      begin #10 timer_fired = 1; end      // the timeout
    join_any
    disable fork;                         // the fix: stop the losing timer explicitly
    #20;
    $display("timer_fired=%0d", timer_fired);
    if (timer_fired) $display("FAIL: join_any only stops waiting; the timer kept running and fired after the response");
    else $display("PASS: the timer was stopped when the response won");
  end
endmodule

Prints: t=2 response arrived timer_fired=0 PASS: the timer was stopped when the response won

Misconception

"join_none children start immediately, in parallel with the next statement."

They are scheduled but do not start until the parent blocks or ends, so code after join_none runs first.

Source IEEE 1800-2017 §9.3.2

Buzzword Without Mechanism

"fork ... join runs its branches in parallel, so their order is random."

They are concurrent, so order within a time step is not guaranteed, but delays still order them in time. "Random" is the wrong word; "unordered within a time slot" is right.

Learn It, Then Prove It