← Design Verification Interviews

Interview Question · Code Reading

Forking Workers Inside a Loop

L0 FresherL1 Juniorsystemverilogprocesseslifetime

The Question

The interviewer asks

What does this print, and how do you make each worker see its own index?

systemverilog
initial begin
  for (int i = 0; i < 3; i++)
    fork
      $display("worker %0d", i);
    join_none
  wait fork;
end
Reveal the Model Answer

It prints worker 3 three times. With join_none, the forked processes start only when the parent blocks at wait fork. By then the loop has finished, and the one loop variable i holds 3. Give each pass its own copy: declare automatic int k = i; inside the fork, before the statement, and print k.

One Loop Variable, Four Processes, All at Time 0
012345ch (loop variable)01234processes forked1234processes running04channel they read-4 4 4 4parent blocks; children start
  1. Cycle 0

    Iteration 0 forks the first child. With join_none the parent does not wait, and the child does not start yet.

  2. Cycle 3

    Iteration 3 forks the fourth child. Four children exist, and none of them has run.

  3. Cycle 4

    ch++ makes ch 4, the loop test fails, and the loop ends.

  4. Cycle 5

    The parent blocks, so the children start. Each one reads ch only now, finds 4, and drives channel 4.

The same race with four children and a loop variable ch. Each column is one step of the parent, all at time 0. The children start only after the loop ends, so all of them read 4.

All workers share i

fork
  $display("worker %0d", i);
join_none

Runs after the loop: prints 3, 3, 3

Each worker owns k

fork
  automatic int k = i;
  $display("worker %0d", k);
join_none

k is set as each fork is created: 0, 1, 2

Self-Check

Tick each key point you covered in your answer.

What Interviewers Listen For (Fresher)

A good fresher answer predicts three 3s. It explains that join_none children start only when the parent process blocks.

Cross-Questions

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

WhyWhat IfProve ItCompare

  1. Forking Workers Inside a Loop
  2. WhyWhy the Automatic Copy Gets the Right ValueL1 Junior

    The statements inside the fork run after the loop. Why does automatic int k = i; still get 0, 1 and 2?

    Reveal the Model Answer

    Declarations at the top of a fork block are initialised when the fork statement runs, in the parent. This happens before any child process starts. So each pass creates a new k holding that pass's i, and the child that later runs refers to its own k.

    Where Each Child Gets Its Channel NumberTwo loops that fork four children: one shares the loop variable ch, one gives each child its own k. Arrows show data: values written into a variable, or read from it by the children.
    Where each child gets its channel numberTwo loops that fork four children: one shares the loop variable `ch`, one gives each child its own `k`. Arrows show data: values written into a variable, or read from it by the children. process Parent loop for (ch = 0; ch < 4;ch++) variable ch static: one variable process Four children drive_channel(...) variable k, one per child automatic int k = ch variable hits[0:4] drives per channel ch++read when a child runsfilled at the forkeach reads its ownhits[c]++
    Read the diagram as text
    • Parent loop: for (ch = 0; ch < 4; ch++)
    • ch: static: one variable
    • Four children: drive_channel(...)
    • k, one per child: automatic int k = ch
    • hits[0:4]: drives per channel
    • Parent loop → ch: ch++
    • ch → Four children: read when a child runs
    • Parent loop → k, one per child: filled at the fork
    • k, one per child → Four children: each reads its own
    • Four children → hits[0:4]: hits[c]++

    A value copied in the parent, at the fork, is safe from the loop that keeps running.

    Self-Check

    Tick each key point you covered in your answer.

    Buzzword Without Mechanism

    "automatic makes the variable thread-safe."

    It gives each activation its own storage. Two processes that share one automatic variable can still race on it.

  3. WhyWhy Every Child Sees the Final IndexL1 Junior

    Without the copy, every child prints 3, not 2. Why the value that ended the loop?

    Reveal the Model Answer

    The loop stops when i becomes 3 and the test fails. The children of join_none start only when the parent blocks, which is after the loop has finished. At that point i holds 3, the value that ended the loop. So every child prints 3.

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "Each child sees the value of i from the pass that forked it."

    A child reads the shared loop variable when it runs, not when it is forked. By then the loop has ended.

    Source IEEE 1800-2017 §9.3.2

  4. CompareStatic vs Automatic Tasks Called in ParallelL1 JuniorL2 Mid-Level

    Compare a static and an automatic task when two threads call each at the same time. What does this print?

    systemverilog
    module tb;
      // A task declared without 'automatic' in a module is static.
      task wait_and_report(int id, int delay);
        #delay $display("t=%0t static task reports id %0d", $time, id);
      endtask
      task automatic wait_and_report_auto(int id, int delay);
        #delay $display("t=%0t automatic task reports id %0d", $time, id);
      endtask
      initial fork
        wait_and_report(1, 10);
        wait_and_report(2, 20);
        wait_and_report_auto(1, 30);
        wait_and_report_auto(2, 40);
      join
    endmodule
    
    Reveal the Model Answer

    Both static calls report id 2. A static task has one set of arguments shared by every call, and the second call overwrote the first while it waited. The automatic calls report 1 and 2, because each call gets its own arguments. Tasks in modules are static unless declared automatic; class methods are always automatic.

    One Shared Id, or One Id per CallTwo overlapping calls of a task report(id, delay), first as a static task, then as an automatic one. Arrows show data: a call writes its argument into id when it starts, and reads id back when it prints.
    One shared id, or one id per callTwo overlapping calls of a task `report(id, delay)`, first as a static task, then as an automatic one. Arrows show data: a call writes its argument into `id` when it starts, and reads `id` back when it prints. process Call A report(1, 5) process Call B report(2, 1) variable id (static task) one variable for allcalls variable id of call A automatic task variable id of call B automatic task staticstaticautomaticautomatic
    1. Call A startst=2 (illustrative)

      The static report(1, 5) stores 1 in the one id and sleeps for 5 time units.

      Follow the calls, values and waits
      1. data 1: Call A → id (static task)
      2. Waiting: Call A — sleeps until t=7
    2. Call B startst=3 (illustrative)

      report(2, 1) stores 2 in the same id. The 1 that call A stored is gone.

      Follow the calls, values and waits
      1. data 2: Call B → id (static task)
      2. Waiting: Call B — sleeps until t=4
    3. Call B printst=4 (illustrative)

      Call B reads id and prints id=2, which happens to be right.

      Follow the calls, values and waits
      1. data 2: id (static task) → Call B
      2. Released: Call B
    4. Call A wakest=7 (illustrative)

      Call A reads the same id and prints id=2 as well.

      Follow the calls, values and waits
      1. data 2: id (static task) → Call A
      2. Released: Call A
    5. Automatic: a fresh id per callt=7, t=8 (illustrative)

      report_a is declared automatic, so each call creates its own id. Call B writes 2 into its own variable and cannot touch call A's 1.

      Follow the calls, values and waits
      1. data 1: Call A → id of call A
      2. data 2: Call B → id of call B
      3. Waiting: Call A — sleeps until t=12
      4. Waiting: Call B — sleeps until t=9
    6. Each call prints its own valuet=9, t=12 (illustrative)

      Call B prints id=2 at t=9. Call A wakes at t=12, reads its own id, and prints id=1.

      Follow the calls, values and waits
      1. data 2: id of call B → Call B
      2. data 1: id of call A → Call A
      3. Released: Call A
      4. Released: Call B
    Read the diagram as text
    • Call A: report(1, 5)
    • Call B: report(2, 1)
    • id (static task): one variable for all calls
    • id of call A: automatic task
    • id of call B: automatic task
    • Call A → id (static task): static
    • Call B → id (static task): static
    • Call A → id of call A: automatic
    • Call B → id of call B: automatic

    Same calls, same times. Only the lifetime of id differs.

    RUNVerilator · simulator service
    Edit the code, then run it
    Expected: t=10 static task reports id 2 t=20 static task reports id 2 t=30 automatic task reports id 1 t=40 automatic task reports id 2

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "Each call to a task gets its own copy of the arguments."

    Only automatic tasks do. A static task's arguments live in one place, so concurrent calls overwrite each other, as the run shows.

    Source IEEE 1800-2017 §13.3.2

  5. Prove ItSolve 3.2.1: Invocation-Local AccumulatorSimulator-graded

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

    Open Problem 3.2.1

  6. Prove ItSolve 3.2.1: Invocation-Local AccumulatorSimulator-graded

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

    Open Problem 3.2.1

  7. What IfWhat Wait Fork Waits ForL1 JuniorL2 Mid-Level

    Asked if they drop the wait fork

    What if the wait fork is removed, or the loop is moved into a task that the test calls? What does wait fork actually wait for?

    Reveal the Model Answer

    Without it, the initial block ends while the workers still run. Nothing waits for them, so a test can finish in the middle of their work. wait fork waits for every child process of the calling process. That includes children forked earlier for other reasons, such as a background monitor that never ends. Wrap the workers in their own fork begin ... end join when you only mean them.

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "wait fork waits for the fork statement just above it."

    It waits for all immediate children of the current process, whichever fork created them.

    Source IEEE 1800-2017 §9.6.1

What People Get Wrong

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

Misconception

"Each worker keeps the value of i from the pass that forked it."

The forked statement reads i when it runs, not when it was forked, and they all run after the loop. The twin prints three 3s.

Run It

The interview code, with a check on what the first worker saw.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: worker 3 worker 3 worker 3 sum of the indexes the workers saw: 9 FAIL: the workers start after the loop ends and all read the one shared i, now 3

Tool as Truth

"It prints 0, 1, 2 on Verilator, so the code is fine."

Verilator 5.052 prints 0, 1, 2 for this code, while the standard's own example in §9.3.2 needs the automatic copy to get distinct values. Icarus 12 prints 3, 3, 3.

Source IEEE 1800-2017 §9.3.2; measured: Verilator 5.052 prints worker 0, 1, 2, Icarus 12 prints worker 3 three times

Half-Truth

"Use join instead of join_none to fix it."

That does print 0, 1, 2, but the workers then run one after another, which defeats the point of forking them in parallel.

Learn It, Then Prove It