A semaphore is like a bucket with a fixed number of keys. A process has to get a key from the bucket before it can continue, and other processes have to wait until a key becomes available. Semaphores are used for mutual exclusion, controlling access to a shared resource, and basic synchronization between processes.

7 min read | Intermediate Level

What You'll Learn

  • The semaphore syntax and its four built-in methods
  • How a single-key semaphore enforces mutual exclusion (a mutex)
  • try_get(), the non-blocking way to check for a key without waiting
  • How a semaphore with more than one key lets several processes proceed at once
Processes get keys from a semaphore bucket

Syntax


semaphore 	[identifier_name];

semaphore is a built-in class, so it's used just like any other class object — declared, then constructed with new().

Methods

MethodDescription
function new (int keyCount = 0);Specifies the number of keys initially allocated to the bucket
function void put (int keyCount = 1);Returns the given number of keys to the bucket
task get (int keyCount = 1);Blocks until the given number of keys can be obtained from the bucket
function int try_get (int keyCount = 1);Attempts to obtain keys immediately, without blocking; returns 1 on success, 0 if not enough keys are available

get() is a task because it can block; put() and try_get() are both functions because neither one ever waits — try_get() just reports failure immediately instead of blocking.

A Basic Example: Mutual Exclusion With One Key

With a single key, a semaphore behaves as a mutex — only one process can hold it at a time. Here, two people compete for the same library room, which only has one key available:


module tb_top;
   semaphore key;

   initial begin
      key = new (1);
      fork
         personA ();
         personB ();
         #25 personA ();
      join_none
   end

   task getRoom (bit [1:0] id);
      $display ("[%0t] Trying to get a room for id[%0d] ...", $time, id);
      key.get (1);
      $display ("[%0t] Room Key retrieved for id[%0d]", $time, id);
   endtask

   task putRoom (bit [1:0] id);
      $display ("[%0t] Leaving room id[%0d] ...", $time, id);
      key.put (1);
      $display ("[%0t] Room Key put back id[%0d]", $time, id);
   endtask

   task personA ();
      getRoom (1);
      #20 putRoom (1);
   endtask

   task personB ();
      #5  getRoom (2);
      #10 putRoom (2);
   endtask
endmodule

Output

Output
[0] Trying to get a room for id[1] ...
[0] Room Key retrieved for id[1]
[5] Trying to get a room for id[2] ...
[20] Leaving room id[1] ...
[20] Room Key put back id[1]
[20] Room Key retrieved for id[2]
[25] Trying to get a room for id[1] ...
[30] Leaving room id[2] ...
[30] Room Key put back id[2]
[30] Room Key retrieved for id[1]
[50] Leaving room id[1] ...
[50] Room Key put back id[1]

personA grabs the only key immediately at 0ns. personB's attempt at 5ns blocks on key.get(1), since nothing's available — it doesn't succeed until personA returns the key at 20ns. The same pattern repeats when personA's second call competes with personB still holding the key at 25ns.

Quick Check

Q: With only one key in the bucket, could two processes ever both be inside their "room" (between get() and put()) at the same simulation time?

Show Answer

No. That's exactly what a one-key semaphore guarantees — get() only succeeds once a key is actually available, so a second process attempting get() while the first still holds the only key is forced to block until it's returned.

try_get(): Checking for a Key Without Waiting

Sometimes blocking indefinitely on get() isn't what you want — you'd rather check whether a key is available right now, and do something else immediately if it isn't. That's exactly what try_get() is for:


module tb;
  semaphore key;

  initial begin
    key = new (1);

    fork
      // Process A takes the only key and holds it for 30ns
      begin
        key.get (1);
        $display ("[%0t] Process A acquired the key", $time);
        #30 key.put (1);
        $display ("[%0t] Process A released the key", $time);
      end

      // Process B checks at 10ns, without blocking, whether a key is free
      begin
        #10;
        if (key.try_get (1))
          $display ("[%0t] Process B got the key immediately", $time);
        else
          $display ("[%0t] Process B could not get the key right now - moving on instead of waiting", $time);
      end
    join
  end
endmodule

Process A takes the only key at 0ns and doesn't release it until 30ns. When Process B calls try_get(1) at 10ns, the key isn't available — but instead of blocking like get() would, try_get() returns 0 immediately, and Process B moves straight into the else branch and continues on with whatever comes next, rather than waiting around for the key to free up.

If Process B's check happened at a moment when the key was available, try_get(1) would return 1, actually take the key (just like get() would), and Process B would need to put() it back later — the only difference from get() is what happens when a key isn't available: try_get() fails and returns instantly instead of waiting.

Quick Check

Q: If Process B's try_get(1) check above happened at 35ns instead of 10ns, what would it return?

Show Answer

1 (success). Process A releases the key at 30ns, so by 35ns a key is available again, and try_get(1) would succeed immediately and take it.

Where Is try_get() Used?

try_get() is useful when a component needs to opportunistically use a shared resource only if it's immediately free — falling back to some other action instead of stalling — rather than committing to wait however long it takes for the resource to become available.

A Semaphore With Multiple Keys

A semaphore isn't limited to acting as a mutex — giving it more than one key lets that many processes proceed concurrently, only blocking once every key is checked out:


module tb;
  semaphore pool;

  task automatic use_resource (int id);
    pool.get (1);
    $display ("[%0t] Worker %0d acquired a resource", $time, id);
    #10 pool.put (1);
    $display ("[%0t] Worker %0d released a resource", $time, id);
  endtask

  initial begin
    pool = new (3);   // 3 resources available at once

    fork
      use_resource (1);
      use_resource (2);
      use_resource (3);
      use_resource (4);   // no key left for this one - has to wait
    join
  end
endmodule

With 3 keys available, Worker 1, Worker 2, and Worker 3 all acquire a resource at 0ns without blocking each other at all. Worker 4 has nothing left to take, so it blocks until one of the first three releases its resource at 10ns — at which point Worker 4 immediately acquires it, then releases it again 10ns later, at 20ns.

Common Beginner Mistakes

Mistake #1: Forgetting to put a key back

Every get() (or successful try_get()) needs a matching put(). Why? If a task takes a key and exits early — through an error path, for instance — without returning it, every other process waiting for that key blocks forever, a classic deadlock.

Mistake #2: Using get() when try_get() was actually needed

get() blocks indefinitely if no key is available; try_get() never blocks at all. Why? If the intent was "use the resource only if it's free right now, otherwise do something else," get() is the wrong choice — it commits the calling process to waiting, however long that takes.

Mistake #3: Assuming a semaphore is always a mutex

A semaphore only behaves as a mutex when it's created with exactly one key. Why? With new(3) or any count above 1, multiple processes can legitimately hold a key at the same time — it's a resource pool, not automatically an exclusive lock, unless you specifically construct it with a single key.

Practice Exercise

Challenge: Add a Non-Blocking Check to the Resource Pool

Using the 3-key resource pool example above, add a fifth worker that checks with try_get() at time 0 (alongside the other four) instead of blocking with get(), and prints whether it succeeded.

Hint

At time 0, only 3 keys exist and workers 1 through 3 already claim them — think about whether a try_get() call racing against those same three would have anything left to take.

Solution

task try_use_resource (int id);
  if (pool.try_get (1))
    $display ("[%0t] Worker %0d got a resource immediately", $time, id);
  else
    $display ("[%0t] Worker %0d found no resource available - skipping", $time, id);
endtask

// Add to the fork block:
// try_use_resource (5);

With only 3 keys and 3 other workers already competing for them at the same simulation time, Worker 5's try_get() is very likely to fail and print the "skipping" message — unlike get(), it never sits around waiting to find out if one eventually frees up.

Summary

In this tutorial, you learned:

  • A semaphore is a bucket of keys; a process needs a key to proceed and returns it when done
  • get() blocks until a key is available; put() returns a key; try_get() attempts to get a key without ever blocking
  • A semaphore created with exactly one key behaves as a mutex, allowing only one process at a time
  • A semaphore created with more than one key lets that many processes proceed concurrently before any of them has to wait
  • try_get() is the right tool when a process should use a resource only if it's immediately available, rather than committing to wait for it

Continuing the Interprocess Communication Chapter

Continue with SystemVerilog Mailbox for passing data between components.

Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5