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.

diagram
[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
diagram
[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.

diagram
[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.

systemverilog
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
diagram
[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 loops

What 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

diagram
[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.