Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
    • Classes, Objects & Handles
    • Inheritance and Polymorphism
    • Static, Encapsulation & Parameterized Classes
    • Virtual Interfaces: the Class-to-Pins Bridge
    • Events, Semaphores, Mailboxes and Processes
      • Events: Edge Waits and Level Fixes
      • Semaphores: One Key and Many
      • Mailboxes: Typed Pipes with Backpressure
      • Threads and Control: Fork, Disable, Wait, Process
      • Synchronization Patterns: Handshakes, Phasing and Objections
      • Lab: Generator-to-Driver Handshake
    • OOP Patterns for Testbenches
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Events, Semaphores, Mailboxes and Processes
  1. Topics
  2. SystemVerilog
  3. Object-Oriented SystemVerilog for Verification
  4. Events, Semaphores, Mailboxes and Processes
  5. Semaphores: One Key and Many

Part 2 · OOP for Verification · Intermediate

Semaphores: One Key and Many

Keys, atomic get and put, bus arbitration, deadlock and backoff fixes.

On this page

    Why this matters

    The run hung at the same time step on every seed. Two processes each held one lock and waited forever for the other's.

    After this lesson you can: share a resource with a semaphore, use try_get, and recognise and fix an AB-BA deadlock.

    Keys and atomic get

    A semaphore is a bucket of keys. new(N) creates it with N keys, get(M) blocks until M keys are available and removes them atomically, put(M) returns keys, and try_get(M) probes without blocking.

    When two processes call get(1) in one timestep, exactly one is granted. There is no torn state and no test-and-set race to write by hand.

    Call

    Blocks

    Returns

    get(M)

    Yes, until M keys free

    Nothing, grants M keys

    put(M)

    No

    Nothing, adds M keys

    try_get(M)

    No

    1 on success, 0 when short

    A semaphore has no ownership. Any process may put keys, even one that never called get, and putting more than the initial count is legal, so get-then-put pairing is a convention for review to enforce.

    One key as a mutex

    The standard testbench use is two bus drivers sharing one physical bus. Each takes the single key before driving and returns it after, so overlapping drives become impossible.

    Blocked getters wake in FIFO order of arrival (IEEE 1800 15.3.3). The arrival order of processes in the same timestep is indeterminate, and the LRM does not promise fairness for a new get() whose keys are already free. Designs that need a policy must add explicit arbitration.

    Predict first In the bench below both drivers call get(1) at time 0 on a one-key semaphore and hold the bus for 20 ns. When does each one get it?

    One at 0 ns and the other at 20 ns, when the first puts the key back. Which driver goes first is not defined; that only one holds the bus at a time is.

    Two drivers share one bus through a one-key semaphore:

    top_sem.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    timeunit 1ns;      // one time unit for the classes and the module
    timeprecision 1ps;
    
    class bus_driver_c;
      string name;
      semaphore bus_sem;
      time granted_at;
      function new(string n, semaphore s);
        name = n;
        bus_sem = s;
      endfunction
      task drive_one(int unsigned addr);
        bus_sem.get(1);
        granted_at = $time;
        $display("[%0t] %s: GRANTED addr=%0h", $time, name, addr);
        #20;
        bus_sem.put(1);
      endtask
    endclass
    module top_sem;
      initial begin
        semaphore sem;           // declare first: initializers here would run at time 0
        bus_driver_c a, b;
        sem = new(1);
        a = new("drvA", sem);
        b = new("drvB", sem);
        fork
          a.drive_one('h10);
          b.drive_one('h20);
        join
        if (a.granted_at == b.granted_at) $fatal(1, "FAIL: both drivers held the bus at %0d ns", a.granted_at);
        $display("PASS: one key serialized the two drivers");
        $finish;
      end
    endmodule
    Terminal
    [0] drvA: GRANTED addr=10
    [20000] drvB: GRANTED addr=20
    PASS: one key serialized the two drivers
    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // snippet
    // Fix: try_get with backoff. Never hold one lock while blocking on another.
    task acquire_both(semaphore s_bus, semaphore s_mem);
      forever begin
        s_bus.get(1);
        if (s_mem.try_get(1)) return;
        s_bus.put(1);
        #1;
      end
    endtask

    Deadlock and fixes

    With two semaphores, process P can hold the bus lock while wanting memory, and Q can hold memory while wanting the bus. Neither proceeds and the run hangs at its last delay.

    AB-BA deadlock: each process holds the lock the other one is waiting for.

    Three cures exist: one global lock order for every process, try_get with backoff as above, or one coarse lock for both resources. Ordering is cheapest, backoff keeps the most parallelism.

    Common pitfall

    A missing put on any early return leaks the key and every later get hangs. A stray extra put silently grows the bucket and mutual exclusion disappears, so count the pairs in review.

    Interview

    Likely questions: mutex versus semaphore, syncing two agents on one bus, and a hang at the same time each run.

    • A 1-key semaphore acts as a mutex but has no owner.

    • One shared handle, with get and put around the drive task.

    • Suspect a missing put or an AB-BA lock order clash.

    Check yourself Two getters block on an empty 1-key semaphore and one key is put. What is guaranteed about who wakes?

    Exactly one wakes and the bucket is consistent. Blocked getters wake in FIFO order of arrival, but arrival order within one timestep is indeterminate, so order-sensitive code needs its own arbiter.

    Check yourself Process P holds lock A and blocks on B while Q holds B and blocks on A. Name the fault and two fixes.

    AB-BA deadlock. Fix with a global lock order, try_get with backoff, or one coarse lock for both.

    Without a semaphore: a token bit

    The same exclusion with a plain token bit, for tools without semaphores. The busy count proves the drivers never overlap, and both complete. This hand-written test-and-set works because simulators do not suspend a process between wait (token == 1) and token = 1'b0; the LRM does not promise that, while get() is atomic by definition.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    module tb_mutex_token;
      timeunit 1ns;
      timeprecision 1ps;
      bit token = 1;
      int busy = 0;
      int done_count = 0;
      int errors = 0;
      task automatic drive_one;
        wait (token == 1);
        token = 1'b0;
        busy++;
        if (busy !== 1) errors++;
        #20;
        busy--;
        token = 1'b1;
        done_count++;
      endtask
      initial begin
        fork
          drive_one;
          drive_one;
        join
        if (done_count !== 2) errors++;
        if (busy !== 0) errors++;
        if (errors == 0) $display("PASS: 2 drivers shared 1 bus with no overlap");
        else $fatal(1, "FAIL: done=%0d busy=%0d", done_count, busy);
        $finish;
      end
    endmodule
    Terminal
    PASS: 2 drivers shared 1 bus with no overlap

    Remember

    • get(n) waits for n keys and takes them atomically; put(n) returns them.

    • A semaphore has no owner: pair every get with a put yourself.

    • Two locks need one global order, a try_get backoff, or one coarse lock.

    Rule of thumb: take locks in one order everywhere.

    How this was checked

    Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.

    Blocked getters wake in arrival order (IEEE 1800 15.3.3), but arrival order within one time step is not defined; the bench checks only that the drivers never overlap.

    Related topics

    • Next: Mailboxes

    Previous

    55 / 259

    Next: Mailboxes: Typed Pipes with Backpressure

    Up next

    Up nextMailboxes: Typed Pipes with Backpressure

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Threads and Control: Fork, Disable, Wait, Process
    • Synchronization Patterns: Handshakes, Phasing and Objections
    • Lab: Generator-to-Driver Handshake