Part 4 · TLM & Analysis · Intermediate
Producer-Consumer FIFO: Thread Decoupling and Throughput Stability
Using FIFO boundaries to decouple bursty producers from variable-latency consumers and reason about occupancy, throughput, and scheduling.
Decoupling as a concurrency tool
Without a queue, producer and consumer lifecycles are tightly coupled. A FIFO creates a temporal buffer so each side can progress at its own cadence while preserving transaction order.
This is not just convenience: it stabilizes simulation behavior when consumer work fluctuates due to predictor lookups, file I/O, model compute, or random latency in dependent threads.
[TLM] direct call coupling vs FIFO decoupling
DIRECT:
producer thread -> consumer function/task immediately
producer progress depends on consumer completion
FIFO:
producer thread -> enqueue -> continue
consumer thread -> dequeue when ready
queue absorbs short-term rate mismatch[TLM] conceptual scheduler view
fork
producer forever loop:
sample/generate
put()
consumer forever loop:
get()
process
join_none
Producer and consumer are independent run-phase actors.Rate mismatch scenarios
Burst producer, steady consumer
traffic monitor emits many transactions in a short window.
scoreboard consumes at fixed processing cost per item.
FIFO occupancy spikes then drains after burst ends.
Steady producer, variable consumer
consumer sometimes waits on reference model state or external score queues.
occupancy oscillates based on temporary consumer slowdowns.
bounded depth can expose bottlenecks through producer blocking.
Slow producer, fast consumer
consumer mostly blocked at get().
queue used() near zero is healthy in this operating point.
timeouts should key off prolonged no-producer activity, not empty queue alone.
[TLM] occupancy traces by workload
case A (burst producer):
used: 0 1 2 4 7 8 7 6 4 2 0
case B (variable consumer):
used: 0 1 2 3 2 4 5 3 2 1
case C (slow producer):
used: 0 0 1 0 0 1 0 0 1 0
Interpret occupancy with workload context, not by raw value alone.Implementation pattern with observability
A production-safe pattern includes occupancy logging, basic health assertions, and explicit stop behavior.
task run_phase(uvm_phase phase);
bus_txn t;
int unsigned sample_count;
phase.raise_objection(this);
fork
begin : producer_thread
repeat (1000) begin
t = bus_txn::type_id::create("t");
assert(t.randomize());
fifo.put(t);
sample_count++;
if ((sample_count % 100) == 0)
`uvm_info("FIFO_OCC",
$sformatf("after %0d puts, used=%0d", sample_count, fifo.used()),
UVM_LOW)
end
end
begin : consumer_thread
bus_txn c;
forever begin
fifo.get(c);
process_txn(c);
end
end
join_any
disable fork;
phase.drop_objection(this);
endtask[TLM] run-phase decoupling checkpoints
1) producer and consumer each have explicit forever/repeat contract
2) queue occupancy logged on predictable cadence
3) objection scoped to active data movement window
4) shutdown path avoids orphan consumer loopsWhat to monitor in regressions
max occupancy per test/seed to validate sizing assumptions.
average occupancy as coarse pressure indicator.
blocked put durations in bounded FIFOs.
consumer starvation intervals (time since last get).
Common anti-patterns and corrections
[TLM] anti-patterns
ANTI-PATTERN 1:
consumer does busy poll:
forever if (fifo.try_get(t)) process(t);
result: zero-time spinning, poor sim performance
FIX:
use blocking get() in a task loop.
ANTI-PATTERN 2:
producer drops transaction on full try_put failure silently
FIX:
count and report drops or use blocking put with bounded depth.
ANTI-PATTERN 3:
no liveness telemetry
FIX:
periodic used() logging + watchdog timers.Key takeaways
FIFO decoupling stabilizes throughput under thread-rate mismatch.
Occupancy is a first-class health signal in producer-consumer systems.
Bounded queues expose pressure; unbounded queues hide pressure but risk memory growth.
Common pitfalls
Interpreting temporary occupancy peaks as failure without workload context.
Busy polling try_get() loops that starve simulator performance.
No exit strategy for forever consumer loops during phase shutdown.