Skip to practice questions

Communication, synchronization, and process lifetime

SystemVerilog Concurrency

Build deterministic parallel testbench behavior by making communication, resource ownership, wakeup persistence, parentage, and cancellation explicit.

A practical guide to mailboxes, semaphores, events, fork/join variants, process trees, wait fork, disable fork, process handles, barriers, races, and shutdown.

21 direct practice links6 connected chaptersReviewed July 2026
Process and scheduler labOwnership follows the runtime tree.
value / handle channeltxnspace
P0phase owner

Creates immediate children, owns their completion policy, and performs cleanup.

wait fork sees
P1 + P2
targeted control
wait fork observes P1 and P2; disable fork reaches P0's live descendant subtree, while a handle targets one process.

Question-first preparation

Start with a curated practice sequence.

Begin with one representative question from each mechanism, then expand the complete directory when you need a targeted drill. Premium status is shown before you leave this guide.

Recommended start

One representative prompt from each major mechanism. Open any card in the live question bank.

  1. Q161Synchronize a one-outstanding producer and consumerTestbench Concurrency · MediumPractice
  2. Q642EventsVerification Utilities · EasyPremium
  3. Q966Fork-join_anyVerification Utilities · EasyPremium
  4. Q230Make the allocator thread-safeMemory Systems · MediumPractice
  5. Q730MailboxesVerification Utilities · EasyPremium
  6. Q626SemaphoresVerification Utilities · EasyPremium
Complete question directoryFilter every stable practice link by mechanism.21 questions
Showing 21 questions across every track.Choose a track to narrow the practice set without changing its stable question links.
  1. Q161Synchronize a one-outstanding producer and consumerTestbench Concurrency · MediumPractice
  2. Q230Make the allocator thread-safeMemory Systems · MediumPractice
  3. Q730MailboxesVerification Utilities · EasyPremium
  4. Q626SemaphoresVerification Utilities · EasyPremium
  5. Q1055Bounded mailboxesVerification Utilities · EasyPremium
  6. Q650Mailbox peek and blocking putVerification Utilities · MediumPremium
  7. Q642EventsVerification Utilities · EasyPremium
  8. Q758Event persistence within a time slotVerification Utilities · MediumPremium
  9. Q1076Nonblocking event triggersVerification Utilities · HardPremium
  10. Q954Fix event-loss and shared-state monitor bugsVerification · MediumPremium
  11. Q007Build a synchronous FIFO and race-free BFMRTL Design · MediumPractice
  12. Q966Fork-join_anyVerification Utilities · EasyPremium
  13. Q330Fork-join_noneVerification Utilities · MediumPremium
  14. Q887Wait forkVerification Utilities · EasyPremium
  15. Q599Reason about same-region forked output orderSystemVerilog Scheduling · EasyPremium
  16. Q842Capture per-iteration values in fork...join_noneTestbench Concurrency · MediumPremium
  17. Q313Nested disable forkSystemVerilog · HardPremium
  18. Q967Race a transaction against a scoped watchdogTestbench Concurrency · MediumPremium
  19. Q987Control a background process lifecycleTemporal Checks · MediumPremium
  20. Q886Stop the losing forked processSystemVerilog · MediumPremium
  21. Q277UVM global heartbeat componentTemporal Checks · MediumPractice

One process tree · four liveness obligations

Follow every spawned process to an owned terminal state.

Parallel syntax is only the beginning. Deterministic concurrency requires an explicit runtime tree, bounded waits, persistent progress, targeted cancellation, and cleanup.

  1. 01Spawn

    Draw immediate children and unrelated processes.

  2. 02Block

    Name the message, permit, event, or condition that releases each wait.

  3. 03Synchronize

    Preserve progress for both early and late observers.

  4. 04Clean up

    Terminate owned work and return every held resource.

01 · Message transfer

A mailbox transfers a value or handle; ownership remains a contract.

Mailbox put/get operations provide atomic communication and optional capacity. Correctness still depends on producer count, consumer count, termination, type safety, and whether sent objects are copied or shared.

Rule to say firstUse typed, bounded mailboxes when capacity is architectural, pair every blocking put with eventual space and every get with eventual data, and explicitly define whether a transferred handle is shared, frozen, or cloned.

Bounded channel

Producer → capacity → consumer

  1. 01Producerput(txn)

    Blocks when the bounded mailbox is full.

  2. 02Mailbox0…N entries

    Atomic ordering and capacity, but no automatic end-of-stream.

  3. 03Consumerget(txn)

    Blocks when empty and removes the oldest message.

  4. 04Shutdownexplicit token

    A sentinel, count, or phase protocol terminates both sides.

Producerput(txn)

Blocks when the bounded mailbox is full.

new(0)
Unbounded
It removes backpressure and can hide runaway production.
peek
Non-consuming
The item remains, so another consumer can change subsequent behavior.
Class item
Handle transfer
Clone for independent ownership or freeze after send.

A mailbox is a synchronization and communication boundary. Capacity, end-of-stream behavior, and any separate ownership handoff belong in its public contract.

01

Observe

Count producers, consumers, maximum outstanding messages, blocking operations, termination tokens, and shared class handles.

02

Decide

Choose blocking put/get for backpressure, try_put/try_get for explicit polling, and peek only when consuming later is safe.

03

Failure

A final put can deadlock after consumers stop, and sending a class handle can create a race if the producer mutates the same object afterward.

04

Prove

Assert bounded occupancy, count sends and receives, run empty/full schedules, and poison or clone transferred objects according to ownership policy.

Blocking operations create a liveness graph

put waits for capacity, get waits for an item, and peek waits for an item without removing it. A system is live only when another active process can satisfy each wait. Count operations along every shutdown path, not just the steady-state loop.

Typed channel with explicit completionsystemverilog
typedef struct {
  bit last;
  int value;
} item_t;

mailbox #(item_t) channel = new(4);

task automatic produce();
  for (int i = 0; i < 10; i++)
    channel.put('{last: 0, value: i});
  channel.put('{last: 1, value: 0});
endtask

task automatic consume();
  item_t item;
  forever begin
    channel.get(item);
    if (item.last) break;
    check(item.value);
  end
endtask

Operation counts expose deadlock

With a bounded mailbox, two gets do not guarantee that three later puts can complete. If the consumer stops after its second get, the final put may wait forever once capacity is exhausted. Build a balance table that includes the channel's initial and final occupancy.

Mailbox operation semantics
MethodBlocks?Removes?Use
putIf fullAddsBackpressured send
try_putNoAdds on successExplicit retry or drop policy
getIf emptyYesReturns a value or handle; no automatic clone or ownership transfer
try_getNoOn successNonblocking receive
peekIf emptyNoInspect next item
try_peekNoNoConditional inspection
numNoNoDiagnostic snapshot, not atomic admission

Explain it out loud

Interview reasoning checkpoints

Strong answer

A mailbox provides atomic put/get operations and blocking synchronization. A shared queue still needs locking, notification, capacity, and shutdown logic around every access.

Reason it through
  1. Identify the shared-state race around size and pop.
  2. Choose mailbox blocking or nonblocking operations deliberately.
  3. Define capacity and object ownership after transfer.
Interviewer lens

Good answers connect communication with synchronization, not only container syntax.

Common trap

Assuming mailbox transfer deep-copies a class object.

Strong answer

Balance all puts and gets, include initial capacity and shutdown messages, and show that every blocking operation has a concurrently runnable process capable of satisfying it.

Reason it through
  1. Draw a wait-for graph for full and empty states.
  2. Check the terminal path as carefully as the steady state.
  3. Use watchdogs and occupancy assertions to expose violations.
Interviewer lens

The key concept is liveness across all schedules, not merely matching source-code counts.

Common trap

Counting two gets and two puts while ignoring a final sentinel put.

02 · Resources and notifications

A semaphore protects capacity; an event announces a moment.

Semaphores count permits and events publish transient notifications. Neither stores arbitrary state, so late arrivals, fairness, reset, and cancellation need separate design.

Rule to say firstUse semaphores for quantified resource ownership, events for same-slot notification, and persistent state or a mailbox when a late waiter must not miss completion.

Synchronization selector

Permit, pulse, or persistent condition

  1. 01Semaphorecounted keys

    Acquire N units; release exactly N units.

  2. 02Event @enext trigger

    Waiter must already be listening.

  3. 03e.triggeredcurrent time slot

    True only for the remainder of the slot in which e fired.

  4. 04Persistent statebit / count / mailbox

    Late observers can discover that completion already occurred.

Semaphorecounted keys

Acquire N units; release exactly N units.

Fairness
Not guaranteed
Do not depend on a semaphore waking waiters in arrival order.
->>
Nonblocking trigger
Schedules an event trigger without blocking the caller.
Reset
Explicit
Neither an event nor leaked permit repairs itself between tests.

Choose a primitive whose memory matches the protocol: count, transient notification, or persistent completion.

01

Observe

For each wait, ask whether the condition is a resource count, a transient occurrence, or a state that must remain true for late observers.

02

Decide

Use get/put for permits, @event for the next trigger, wait(event.triggered) for current-slot observation, and a bit/counter/mailbox for persistence.

03

Failure

A late @event waiter misses an earlier trigger, and a missing semaphore put leaks capacity until all later users block.

04

Prove

Track permits with assertions, randomize waiter arrival, test trigger-before-wait and wait-before-trigger, and verify reset and cancellation paths.

Semaphores model divisible resources

new(K) creates K keys. get(N) blocks until N are available and then atomically removes them; try_get(N) returns immediately; put(N) returns capacity. The owner must release permits on every normal, error, timeout, and cancellation path.

Release permits on every pathsystemverilog
semaphore ports = new(2);

task automatic use_port(input int id);
  bit acquired = 0;
  ports.get(1);
  acquired = 1;
  begin
    automatic int status = transact(id);
    if (status != 0) $error("transaction %0d failed", id);
  end
  if (acquired) ports.put(1);
endtask

Event observation is deliberately ephemeral

@done waits for a future trigger and misses one that already happened. wait(done.triggered) can observe a trigger anywhere in the current time slot, but the property is not sticky across time slots. Use a persistent done bit, counter, barrier, or message when completion must be remembered.

Notification choices
NeedPrimitiveLate waiter behavior
Next occurrence@eventMisses earlier trigger
Occurrence in current slotwait(event.triggered)Sees same-slot trigger only
Remembered completionbit/counter plus eventReads state immediately
One payload per notificationmailboxPayload remains queued
N arrivals before releasebarrier/counter protocolState persists until phase advances

Explain it out loud

Interview reasoning checkpoints

Strong answer

No. It is true only for the remainder of the simulation time slot in which the event triggered. Use separate state when a waiter arriving later must still observe completion.

Reason it through
  1. Determine whether trigger and wait can occur in either order.
  2. If only same-slot order varies, triggered can remove that race.
  3. If time can advance, store completion in a bit, count, phase, or mailbox.
Interviewer lens

The expected phrase is “current time slot,” not “one clock cycle.”

Common trap

Treating triggered like a sticky boolean property.

Strong answer

No portable fairness guarantee should be assumed. A semaphore guarantees atomic key accounting, not an arrival-ordered arbiter.

Reason it through
  1. Separate mutual exclusion or capacity from scheduling policy.
  2. If fairness matters, place requests in an explicit ordered queue.
  3. Assert bounded wait and starvation requirements at that higher layer.
Interviewer lens

Strong answers distinguish resource safety from progress fairness.

Common trap

Building protocol correctness around the observed wakeup order of one simulator.

03 · Fork and join semantics

Choose when the parent continues and who waits for the rest.

join, join_any, and join_none all create child processes; they differ only in the condition that releases the parent. That condition changes start time, variable capture, and cleanup obligations.

Rule to say firstUse join for a complete parallel phase, join_any only with an explicit policy for surviving children, and join_none only when a later synchronization point owns completion.

Parent release condition

Same children, different continuation

Child A · #12
runs t0→t12runs t0→t12runs after parent yields
Child B · #5
runs t0→t5runs t0→t5runs after parent yields
Child C · #0
finishes at t0finishes at t0runs after parent yields
The same three immediate children exist under every join variant; only the parent-release condition changes.
Parent resumes
t12 · all completet0 · first completeimmediately
With join_none, child execution begins when the continuing parent next blocks or terminates.
  1. 01joinall finish

    Parent resumes after the slowest immediate child.

  2. 02join_anyfirst finishes

    Other children continue unless explicitly stopped or awaited.

  3. 03join_noneparent does not wait

    Children begin when the parent next blocks or terminates.

joinall finish

Parent resumes after the slowest immediate child.

Child start
Parent yields
With join_none, creation precedes execution until the parent blocks or ends.
Loop capture
automatic local
Create per-iteration storage inside the forked context.
Same-time wakeup
Potential race
No deterministic order exists within the same event region.

A fork creates topology. The join keyword states only when the parent resumes, not how surviving children are managed.

01

Observe

Mark the parent process, each immediate child, the first statement after the fork, and which child lifetimes may outlast local variables.

02

Decide

Select a join variant from the required parent-release condition, then add wait fork, process handles, or scoped cancellation for remaining children.

03

Failure

join_any leaves siblings running, and join_none loop bodies can all observe the loop's final value unless each iteration captures an automatic local.

04

Prove

Draw absolute start/finish times, log parent and child IDs, randomize delays, and assert every spawned child reaches completion or a reviewed cancellation path.

join_none startup is deferred until the parent yields

The parent schedules children and continues. Children start when the parent reaches a blocking statement or terminates; an artificial #0 is not required if a real blocking point already exists. Without one, a loop can finish spawning before any child runs.

Capture a per-iteration valuesystemverilog
for (int i = 0; i < 8; i++) begin
  fork
    automatic int lane = i;
    drive_lane(lane);
  join_none
end

wait fork; // waits for this process's immediate outstanding children

Same-time branches have no source-order priority

When two child processes wake in the same event region, their execution order is not defined. A branch that writes x with blocking assignment and another that evaluates y <= x can capture either old or new x depending on scheduling.

Join selection
IntentConstructRequired follow-up
Parallel work, then combine allfork…joinNone beyond result checks
Race worker versus timeoutjoin_anyCancel or await survivor
Launch background workersjoin_noneStore handles or later wait fork
Iterative launchjoin_none + automatic captureBound count and completion
One branch containing sequential stepsbegin…end inside forkUse absolute timeline

Explain it out loud

Interview reasoning checkpoints

Strong answer

They start when the parent process next blocks or terminates. The fork statement creates them, but the parent continues until it yields.

Reason it through
  1. Find the first blocking statement after the fork.
  2. Determine what shared variables change before that point.
  3. Capture per-child values automatically before relying on them.
Interviewer lens

This tests scheduler knowledge and explains common loop-capture bugs.

Common trap

Adding #0 reflexively and assuming it creates a portable priority.

Strong answer

They continue running. The parent must deliberately await, cancel, or transfer ownership of every survivor.

Reason it through
  1. Identify which child completed first.
  2. List all remaining live children.
  3. Use handles or a narrow named scope to clean them up without killing unrelated work.
Interviewer lens

The key is that join_any is not race-and-cancel.

Common trap

Calling disable fork without checking the caller's full child scope.

04 · Parentage and wait fork

Process control follows the runtime tree, not visual indentation.

Named blocks help humans and disable statements, but fork parentage is determined by which process executes a fork. wait fork waits for the caller's immediate outstanding children.

Rule to say firstDraw the process tree before using wait fork or disable fork; lexical proximity does not prove parent-child ownership.

Runtime process tree

The caller owns only its immediate children

  1. 01P0phase process

    Calls wait fork.

  2. 02P1 / P2immediate children

    P0 waits until both complete.

  3. 03P1anested descendant

    P1's completion semantics determine whether P0 indirectly waits.

  4. 04Q0unrelated process

    Lexically nearby but not a child of P0; wait fork ignores it.

P0phase process

Calls wait fork.

wait fork
Immediate children
Includes outstanding children created earlier by the same caller.
Named block
Lexical scope
Useful for declarations and targeted disable, not a replacement for topology.
Loop use
Legal but serializing
A wait fork in each iteration can remove intended parallelism.

To predict wait and cancellation behavior, draw arrows for “spawned by” rather than boxes for source indentation.

01

Observe

Assign an identity to the caller, each immediate child, nested child, and long-lived background process.

02

Decide

Use join for local ownership, wait fork for the caller's own immediate children, and handles when individual nested processes need control.

03

Failure

A helper task that calls wait fork can wait on a different child set than the source layout suggests, while an earlier background child can unexpectedly extend the wait.

04

Prove

Log process::self identities and status, isolate ownership in wrapper processes, and assert phase completion counts.

wait fork waits for the caller's outstanding children

The set is not limited to the most recent fork statement. Any unfinished immediate child previously spawned by that process is included. Nested descendants matter only through whether their immediate parent itself remains alive.

Create a wrapper when wait ownership must be narrowsystemverilog
task automatic run_batch();
  fork
    begin : owned_batch
      for (int i = 0; i < 4; i++) begin
        fork
          automatic int id = i;
          worker(id);
        join_none
      end
      wait fork; // only owned_batch's worker children
    end
  join
endtask

Avoid accidental serialization

Placing wait fork inside a launch loop is legal, but each iteration waits for its newly created and any earlier outstanding children before the next launch. To run all iterations concurrently, launch the full set first and wait once from the owning process.

Topology questions
QuestionWhy it mattersEvidence
Who executed fork?Defines the parentProcess tree
Who calls wait fork?Defines waited child setRuntime identity
Did child use join_none?May finish before descendantsNested timeline
Were children spawned earlier?They are included if outstandingCreation log
Does loop wait each time?May serialize workStart/finish trace

Explain it out loud

Interview reasoning checkpoints

Strong answer

It waits for all outstanding immediate child processes of the process that executes wait fork, including children spawned by earlier fork…join_none statements.

Reason it through
  1. Identify the runtime caller.
  2. Enumerate its immediate unfinished children.
  3. Do not substitute lexical block nesting for process parentage.
Interviewer lens

The expected terms are caller, immediate children, and outstanding.

Common trap

Assuming it waits only for the textually nearest fork.

Strong answer

It can wait at the end of each iteration, preventing the next iteration from launching and therefore serializing work that was intended to overlap.

Reason it through
  1. Draw launch and wait points for two iterations.
  2. Move the wait after all launches if full parallelism is intended.
  3. Bound concurrency explicitly if partial overlap is required.
Interviewer lens

This is legal syntax; the issue is the resulting topology and schedule.

Common trap

Calling it illegal instead of explaining serialization.

05 · Handles and cancellation

Cancel the process you own, not every child in reach.

process handles allow targeted status, await, suspend, resume, kill, and random-state control. disable fork is broader: it kills all active child processes of the caller.

Rule to say firstPrefer stored process handles or a narrow wrapper scope for cancellation; use disable fork only after auditing the caller's entire live child set.

Targeted lifecycle

Capture → inspect → await or kill → verify

  1. 01Captureprocess::self()

    Child publishes its handle before the controller uses it.

  2. 02Inspectstatus()

    FINISHED, RUNNING, WAITING, SUSPENDED, or KILLED guide legal action.

  3. 03Controlawait() / kill()

    Wait naturally or terminate the exact process.

  4. 04Verifyterminal status

    No live child or held resource remains.

Captureprocess::self()

Child publishes its handle before the controller uses it.

Method
await()
The process API uses await, not a join method.
Initialization
Synchronize first
A controller must not call a null handle before its child publishes it.
disable label
Terminates the named scope
Code after a self-terminating named scope may never run.

A process handle turns implicit broad cancellation into a reviewed ownership operation with observable status.

01

Observe

Record when handles are assigned, which process owns them, every possible status, and all siblings that a broad cancellation could reach.

02

Decide

Use await for natural completion, kill for targeted cancellation, and a named wrapper process when a group must stop together.

03

Failure

A watchdog child's disable fork kills its own children, not its siblings; a parent's disable fork can kill unrelated background workers.

04

Prove

Assert handles are non-null before use, check status transitions, run winner and timeout paths, and verify no orphan remains after phase shutdown.

Publish handles before the controller races ahead

A forked child often assigns process::self() after it starts. The parent must wait for that publication or use a handshake before calling methods. With join_none, child startup itself is deferred until the parent yields.

Race work against timeout with targeted cleanupsystemverilog
process worker_p, timer_p;
event handles_ready;

fork
  begin
    worker_p = process::self();
    -> handles_ready;
    do_work();
  end
  begin
    timer_p = process::self();
    #100ns;
  end
join_none

wait (worker_p != null && timer_p != null);
wait (worker_p.status() == process::FINISHED ||
      timer_p.status()  == process::FINISHED);

if (worker_p.status() != process::FINISHED) worker_p.kill();
if (timer_p.status()  != process::FINISHED) timer_p.kill();
wait fork;

disable fork follows caller ownership

When a process executes disable fork, it terminates all active descendants spawned directly by that process, with cascading effects. If a watchdog is merely a sibling of workers, its own disable fork does not reach those siblings. Put the race inside an owning wrapper or retain handles.

Cancellation choices
MechanismTargetPrimary risk
process.kill()One known processHandle publication and resource cleanup
disable forkCaller's active child treeKills unrelated children
disable block_nameAll activations of named scope in reachLexical breadth and self-termination
Cooperative stop flagCode that checks itLatency or blocked wait
Mailbox shutdown tokenChannel consumerOne token per consumer or broadcast design

Cleanup must release resources

Killing a process does not automatically return a semaphore permit, retract a mailbox message, or restore shared state. Structure critical sections so cancellation occurs outside them, or add a controller-owned cleanup protocol.

  • Do not suspend a process while it owns a shared lock unless the design explicitly tolerates it.
  • Use get_randstate/set_randstate only for intentional reproducibility; process random state is part of debug determinism.
  • A disable of a named top scope terminates that scope, so a later display in the killed process does not execute.
  • Avoid nested initial blocks; initial is a module/program/interface construct, not a procedural statement.

Explain it out loud

Interview reasoning checkpoints

Strong answer

It kills all active child processes of the caller, not only the branches from the nearest-looking fork. That may include earlier background children and their descendants.

Reason it through
  1. Identify the process executing disable fork.
  2. Enumerate every live immediate child it owns.
  3. Use a wrapper or handles if the intended target is narrower.
Interviewer lens

A strong answer describes runtime parentage and broad scope.

Common trap

Assuming a watchdog sibling can use disable fork to kill its sibling workers.

Strong answer

First synchronize until the child has published a non-null handle, then call await() or inspect status. The process API method is await(), not join().

Reason it through
  1. Account for deferred child startup under join_none.
  2. Protect the handle publication with a wait, event plus state, or mailbox.
  3. After completion, verify resources and terminal status.
Interviewer lens

This tests both API knowledge and the initialization race.

Common trap

Calling await on a handle before the forked child assigns it.

06 · Integrated concurrency design

Design phases around persistent state, bounded waits, and owned cleanup.

Robust environments combine channels, permits, events, counters, and process handles. The goal is not maximum parallel syntax; it is deterministic progress under every legal interleaving.

Rule to say firstFor each parallel phase, specify entry, progress, completion, timeout, cancellation, and cleanup—and make completion persistent until every required observer acknowledges it.

Phase-control stack

Payload, progress, wakeup, and ownership are separate layers

  1. 01Payloadmailbox

    Ordered work and completion tokens.

  2. 02Resourcesemaphore

    Bounded parallel use of ports, models, or licenses.

  3. 03Progresscount / phase

    Persistent record of arrivals and completion.

  4. 04Wakeupevent

    Efficient notification after state changes.

  5. 05Ownershipprocess handles

    Targeted timeout and shutdown control.

Payloadmailbox

Ordered work and completion tokens.

Safety
Nothing bad happens
No double-consume, permit leak, shared-state race, or invalid access.
Liveness
Something good happens
Every accepted item completes or is explicitly cancelled.
Determinism
All legal schedules agree
Results do not depend on same-region process order.

Different primitives solve different dimensions. Combining them explicitly is clearer than asking one event or one disable statement to carry the whole protocol.

01

Observe

Build a wait-for graph containing processes, mailboxes, permits, event conditions, barriers, timeouts, and shutdown dependencies.

02

Decide

Use messages for payload, counters or barriers for N-way rendezvous, events for wakeup, and handles for ownership-specific cancellation.

03

Failure

A one-shot event loses a late worker, a barrier count never reaches N after a killed process, or a timeout kills the controller before its diagnostic can print.

04

Prove

Run systematic delay permutations, inject cancellation at every blocking point, assert bounded completion, and leave a final zero-live-process and zero-held-resource invariant.

A barrier is persistent progress plus notification

For N participants, store an arrival count or generation number under a synchronization policy. The last arrival advances the generation and triggers a wakeup. Waiters test the persistent generation before and after waiting so an early trigger cannot be lost.

Generation-based rendezvous sketchsystemverilog
class phase_barrier;
  local int target, arrived, generation;
  local semaphore lock = new(1);
  local event advanced;

  function new(int n); target = n; endfunction

  task wait_phase();
    int my_generation;
    lock.get();
    my_generation = generation;
    arrived++;
    if (arrived == target) begin
      arrived = 0;
      generation++;
      -> advanced;
    end
    lock.put();
    wait (generation != my_generation || advanced.triggered);
  endtask
endclass

Debug with absolute time and ownership

Write absolute wakeup times, event regions, and process parentage before reading printed output. For a sequence of #12, then #5, then #0 in one branch, completion times are t12, t17, and t17—not independent delays from t0.

Concurrency debug checklist
DimensionQuestionTypical symptom
TimeAbsolute or relative?Wrong output order prediction
RegionWho wakes in the same region?Nondeterministic captured value
TopologyWho spawned whom?wait/disable reaches wrong processes
PersistenceCan a late waiter observe state?Lost event and hang
CapacityCan every blocking put/get proceed?Terminal deadlock
CleanupWho releases permits and handles?Next test hangs

A timeout is a protocol participant

Place worker and timer under a controller that survives either outcome, records the winner, cancels the loser, releases resources, and reports. If the watchdog disables the controller's named scope from inside it, any reporting code after that disable is also terminated.

  • Use a heartbeat counter or timestamp for persistent health; an event alone can be missed.
  • Bound every wait in testbench infrastructure or pair it with a top-level watchdog that preserves diagnostics.
  • Record message IDs, process IDs, phase generation, and absolute time in debug logs.
  • At end of test, assert no outstanding mailbox work, no held permits, and no live owned processes.

Explain it out loud

Interview reasoning checkpoints

Strong answer

An event records no arrival count or generation and can be missed by late waiters. A barrier needs persistent progress state, with an event used only as a wakeup optimization.

Reason it through
  1. Count how many participants must arrive.
  2. Store the current generation and arrival count.
  3. Recheck persistent state after every wakeup.
Interviewer lens

The key distinction is notification versus remembered condition.

Common trap

Triggering one event N times and assuming each worker receives one trigger.

Strong answer

Define its safety and liveness invariants, remove same-region order dependencies, randomize delays and phase, inject boundary failures, and show every legal schedule yields the same externally visible result.

Reason it through
  1. Model communication and resource ownership separately.
  2. Assert counts, capacity, and bounded completion.
  3. Check final cleanup: no live process, held permit, or pending item.
Interviewer lens

Strong answers go beyond “run it many times” to invariants and schedule independence.

Common trap

Treating a stable result in one simulator seed as proof of race freedom.

Standards and primary references

Check the contract at its source.

IEEE Std 1800-2023 — SystemVerilogNormative reference for processes, fork/join, interprocess communication, events, semaphores, mailboxes, and process control.Accellera event-semantics clarificationClarifies event trigger observation and the current-time-slot behavior of the triggered property.Accellera SystemVerilog draftHistorical specification background for concurrency and interprocess synchronization.