Difficulty: Medium · Puzzle 4 of 6 · Topic: Deadlock Scenario Puzzles: Objection and Phase Stalls
The test passes and prints PASS, but coverage is tiny and the scoreboard saw almost no packets: 'processed 0 of 100'. There is no hang and no fatal. Find the control-flow bug in the producer/consumer task.
mailbox #(pkt) mbx = new(); // unbounded
int processed;
// inside a class (methods are automatic); a module-scope static task would share one p
task run_traffic();
fork
begin : producer
repeat (100) begin
pkt p; // class context, or make the task automatic
p = new(); // a fresh object per iteration
build_pkt(p);
mbx.put(p); // unbounded mailbox: never blocks
end
end
begin : consumer
forever begin
pkt q;
mbx.get(q);
process_pkt(q); // takes time (#10)
processed++;
end
end
join_any
disable producer; // no-op: producer already finished
endtaskWhich of the two branches finishes first, and what does join_any do when it does?
1) Print time and mbx.num() at the end of run_traffic: the end time is 0 and mbx.num() is 99.
2) The producer puts all 100 packets at time 0 (an unbounded mailbox put never blocks) and exits. join_any returns as soon as the producer finishes.
3) disable producer does nothing: it already finished. The consumer is simply idle/busy, it is not deadlocked, and nothing 'died'.
4) The task returns, the test finishes, and 99 packets are never processed (verified in Verilator with the code inside a class: end time 0, processed=0, left=99). Note: a module-scope task is static, so an initialised declaration like pkt p = new() would run once and every put would be the same handle; declare and construct separately inside a class or an automatic task.
5) Fix: wait for the work, not the first thread. Use fork..join with a poison pill (producer puts a null packet, consumer breaks on null), or after the producer finishes wait (mbx.num() == 0 && inflight == 0) before returning; and add an end-of-test check that mbx is empty.Answer: Bug: join_any returns when the producer finishes, ending the task with almost all packets still in the mailbox; the consumer blocking on an empty mailbox is normal idling, nothing is deadlocked. Fix: wait until the mailbox is empty and in-flight work is done (or use a poison pill with fork..join), and check mailbox emptiness at end of test.
Checked by running the code in Verilator 5; the printed output supports the answer above. Plain-SV model of the producer/consumer task: join_any returns with the work unprocessed, a poison pill with wait fork finishes it.
The wrong story (the producer died and the consumer is stuck) sounds plausible but nothing died: the symptom is silent data loss, which is worse than a hang because the test passes.
How would you redesign this with one supervisor thread owning both producer/consumer lifetime? Model answer: A supervisor forks producer and consumer, producer sets a done flag (or sends a poison pill) when finished, the consumer exits when the pill arrives or when done is set and the mailbox is empty, and the supervisor does a plain join then asserts mbx.num()==0; one place decides when the run is over.
I start by asking whether anything is really deadlocked, because a consumer blocked on an empty mailbox is normal idling. The root cause is that join_any returns when the producer finishes, which ends the task with almost all packets still in the mailbox, so data is silently lost and the test passes. I confirm it by checking the mailbox occupancy at the end of the test. The fix is to wait until the mailbox is empty and the in-flight work is done, or to use a poison pill with fork and join.