sc_mutex and sc_semaphore in SystemC — Shared Resource Synchronization
sc_fifo buffers data between a producer and a consumer. But sometimes the problem is different: two processes need to share the same resource — a shared register file, a bus, a memory bank — and only one of them should touch it at a time. That is an access-control problem, not a buffering problem.
SystemC provides two synchronization primitives for this: sc_mutex for mutual exclusion (exactly one thread at a time) and sc_semaphore for counting access (up to N threads at a time). Both are built-in channels, declared at the module or sc_main level, and work only with SC_THREAD processes for their blocking calls.
1. The Shared Resource Problem
Consider two initiator threads that both write to a shared configuration register. Without coordination, their writes interleave in simulation time and corrupt each other — even if each write takes zero time.
// Without synchronization — race condition // Thread A: reg = reg | FLAG_A (read: 0x00, write: 0x01) // Thread B: reg = reg | FLAG_B (read: 0x00, write: 0x02) ← reads stale 0x00 // Result: reg == 0x02 ← FLAG_A is lost
The problem here is not ordering of data — it is ordering of operations. You need a way to say: "while I am touching this register, no one else can." That is exactly what sc_mutex provides.
// With sc_mutex — safe read-modify-write // Thread A: lock → read 0x00, write 0x01 → unlock // Thread B: lock (blocks) → read 0x01, write 0x03 → unlock // Result: reg == 0x03 ← both flags set correctly
2. sc_mutex — Mutual Exclusion
sc_mutex implements the sc_mutex_if interface. At most one thread can hold the lock at any point in simulation time. Any other thread that calls lock() while it is held will block until the owner releases it.
#include <systemc.h> // Declare at module scope or in sc_main sc_mutex bus_lock; // anonymous sc_mutex reg_lock("reg_lock"); // named — preferred
The core API has three methods:
// ── lock() ─────────────────────────────────────────────────── // Acquires the mutex. // If already locked by another thread: BLOCKS until unlocked. // Returns 0 on success. reg_lock.lock(); // ── unlock() ───────────────────────────────────────────────── // Releases the mutex. // Any thread blocked on lock() is woken up. // Returns 0 on success, non-zero if not currently locked. reg_lock.unlock(); // ── trylock() ──────────────────────────────────────────────── // Non-blocking attempt to acquire. // Returns 0 if lock was acquired. // Returns -1 if already locked (does NOT block). if (reg_lock.trylock() == 0) { // got the lock — do work, then unlock reg_lock.unlock(); } else { // resource busy — try again later }
lock() again on the same mutex, it will block waiting for itself — an instant deadlock. There is no owner-check or re-entry counter. Design your critical sections so a thread never tries to acquire a mutex it already holds.
3. sc_mutex in Practice — Protected Register Access
The canonical pattern is: lock, do the critical work, unlock. Wrapping the unlock in a cleanup path is good practice, but in SystemC simulations a thrown exception halts the simulation anyway — so a simple lock/unlock pair is usually fine.
#include <systemc.h> SC_MODULE(SharedReg) { sc_mutex lock; uint32_t reg_val = 0; void thread_a() { while (true) { wait(10, SC_NS); lock.lock(); reg_val |= 0x01; // set FLAG_A cout << "A wrote: 0x" << std::hex << reg_val << "\n"; lock.unlock(); } } void thread_b() { while (true) { wait(10, SC_NS); lock.lock(); reg_val |= 0x02; // set FLAG_B cout << "B wrote: 0x" << std::hex << reg_val << "\n"; lock.unlock(); } } SC_CTOR(SharedReg) : lock("lock"), reg_val(0) { SC_THREAD(thread_a); SC_THREAD(thread_b); } };
Both threads wake at the same simulation time (10 ns). One of them acquires the lock first (simulator picks an order), completes its read-modify-write, and unlocks. The other then proceeds. The final value is always 0x03 — both flags set, no corruption.
4. sc_semaphore — Counting Access
A semaphore maintains an integer count. wait() decrements it — if the count would go below zero, the caller blocks. post() increments it and wakes a blocked waiter. The initial value controls how many threads can proceed concurrently.
#include <systemc.h> // Binary semaphore (initial value 1) — behaves like a mutex sc_semaphore sem_binary("sem_bin", 1); // Counting semaphore (initial value N) — allows N concurrent holders sc_semaphore sem_ports("sem_ports", 4); // 4 DMA channels can run at once
The three methods:
// ── wait() ─────────────────────────────────────────────────── // Decrements count. // If count was already 0: BLOCKS until another thread calls post(). // Returns 0 on success. sem_ports.wait(); // ── post() ─────────────────────────────────────────────────── // Increments count. // If any thread is blocked on wait(): one is woken up. // Returns 0 on success. sem_ports.post(); // ── trywait() ──────────────────────────────────────────────── // Non-blocking decrement. // Returns 0 if count was > 0 (decremented successfully). // Returns -1 if count was 0 (caller is NOT blocked). if (sem_ports.trywait() == 0) { // got a slot sem_ports.post(); // release when done } // ── get_value() ────────────────────────────────────────────── // Returns current count — useful for assertions and debug. int slots_left = sem_ports.get_value();
sc_mutex, a semaphore has no concept of "owner." Any thread can call post() — even one that never called wait(). This means there is no automatic cleanup if you forget to post. An unmatched wait() will drain the count and eventually deadlock all threads trying to enter.
5. sc_semaphore in Practice — DMA Channel Pool
A common use case is a pool of limited hardware resources: DMA channels, interrupt lines, or memory banks with limited ports. The semaphore tracks how many are free.
#include <systemc.h> SC_MODULE(DmaPool) { sc_semaphore channels; // 3 DMA channels available void requester(int id) { while (true) { wait(5, SC_NS); cout << sc_time_stamp() << " CPU" << id << " waiting for DMA\n"; channels.wait(); // block until a channel is free cout << sc_time_stamp() << " CPU" << id << " got DMA, channels left: " << channels.get_value() << "\n"; wait(20, SC_NS); // simulate DMA transfer channels.post(); // release channel cout << sc_time_stamp() << " CPU" << id << " released DMA\n"; } } SC_CTOR(DmaPool) : channels("channels", 3) { SC_THREAD(requester); sensitive << SC_ZERO_TIME; dont_initialize(); // spawn 5 CPU threads competing for 3 channels for (int i = 0; i < 5; i++) { sc_spawn([this, i]{ requester(i); }); } } };
With 5 requesters and 3 channels, at most 3 will ever be active simultaneously. The remaining 2 block on channels.wait() until one of the active transfers calls channels.post().
6. Complete Example — Two-Master Shared Bus
Here is a self-contained simulation: two master threads compete for a single shared bus. Only one can drive the bus at a time. The sc_mutex enforces the arbitration.
#include <systemc.h> #include <iomanip> SC_MODULE(SharedBus) { sc_mutex bus_arb; uint32_t bus_data = 0; void master(const char* name, uint32_t base_addr, sc_time period, sc_time xfer_time) { uint32_t addr = base_addr; while (true) { wait(period); // --- request bus --- cout << std::setw(10) << sc_time_stamp() << " " << name << ": requesting bus\n"; bus_arb.lock(); // --- drive bus --- bus_data = addr; cout << std::setw(10) << sc_time_stamp() << " " << name << ": driving 0x" << std::hex << addr << std::dec << "\n"; wait(xfer_time); // --- release bus --- bus_arb.unlock(); cout << std::setw(10) << sc_time_stamp() << " " << name << ": released bus\n"; addr += 4; } } SC_CTOR(SharedBus) : bus_arb("bus_arb"), bus_data(0) { sc_spawn([this]{ master("CPU0", 0x1000, sc_time(10, SC_NS), sc_time(15, SC_NS)); }); sc_spawn([this]{ master("DMA", 0x2000, sc_time(12, SC_NS), sc_time(8, SC_NS)); }); } }; int sc_main(int, char**) { SharedBus dut("dut"); sc_start(100, SC_NS); return 0; }
Sample output — notice CPU0 blocks at 20 ns waiting for DMA to release the bus:
10 ns CPU0: requesting bus
10 ns CPU0: driving 0x1000
12 ns DMA: requesting bus ← DMA arrives while CPU0 holds lock
25 ns CPU0: released bus
25 ns DMA: driving 0x2000 ← DMA proceeds immediately after unlock
33 ns DMA: released bus
7. sc_mutex vs sc_semaphore — When to Use Which
| Property | sc_mutex | sc_semaphore |
|---|---|---|
| Concurrent holders | Exactly 1 | Up to N (set by initial value) |
| Typical use | Exclusive access to one shared resource | Pool of N identical resources |
| Ownership concept | Yes — only the locker should unlock | No — any thread can post() |
| Recursive locking | Deadlocks — not supported | N/A — no ownership |
| Non-blocking call | trylock() returns -1 if locked | trywait() returns -1 if count == 0 |
| Binary mode | Always binary | Binary when initial value = 1 |
| Query state | Not available | get_value() returns current count |
| Blocking call | lock() — SC_THREAD only | wait() — SC_THREAD only |
Use sc_mutex when there is one shared thing and one thread must own it exclusively. Use sc_semaphore when you have a pool of N resources and want to allow up to N concurrent consumers with blocking when all are busy.
8. Common Pitfalls
Deadlock from missing unlock
If a thread acquires a mutex and then calls wait() on a signal or timer without ever unlocking, any other thread that later calls lock() will block forever. The simulator will eventually report no runnable processes.
// BAD — deadlock if signal never fires void thread_a() { lock.lock(); wait(done_signal); // ← still holding lock — thread_b can never enter lock.unlock(); } // GOOD — unlock before the long wait void thread_a() { lock.lock(); do_critical_work(); lock.unlock(); // ← release before sleeping wait(done_signal); }
Deadlock from recursive locking
This is the most common bug with sc_mutex. If funcA() locks a mutex and then calls funcB() which also tries to lock the same mutex, the thread blocks waiting for itself.
// BAD — recursive lock → instant deadlock void funcA() { lock.lock(); funcB(); lock.unlock(); } void funcB() { lock.lock(); /* DEADLOCK */ lock.unlock(); }
Semaphore count going negative — impossible, but blocking looks like it
The count never goes below zero in the semaphore itself — when the count is zero, wait() blocks the calling thread instead of decrementing. If you see all threads blocked, check that every wait() is eventually matched with a post(). Use get_value() in a watcher thread to assert the count stays in the expected range.
// Debug watcher — assert semaphore count never exceeds initial value void watcher() { while (true) { wait(1, SC_NS); assert(sem.get_value() <= 4); // initial value was 4 assert(sem.get_value() >= 0); } }
API Reference
| Method | Type | Behaviour |
|---|---|---|
sc_mutex::lock() | Blocking | Acquires mutex — blocks if held by another thread; returns 0 |
sc_mutex::unlock() | Non-blocking | Releases mutex; returns 0 on success, non-zero if not locked |
sc_mutex::trylock() | Non-blocking | Returns 0 if acquired, -1 if already locked |
sc_semaphore::wait() | Blocking | Decrements count — blocks if count is 0; returns 0 |
sc_semaphore::post() | Non-blocking | Increments count and wakes a waiter if any; returns 0 |
sc_semaphore::trywait() | Non-blocking | Returns 0 if count > 0 (decremented), -1 if count was 0 |
sc_semaphore::get_value() | Non-blocking | Returns current count — useful for debug assertions |
Summary
sc_mutexprovides mutual exclusion — at most oneSC_THREADholds the lock at any simulation time.sc_mutexis not recursive — a thread locking a mutex it already holds deadlocks immediately.sc_semaphoreis a counting primitive — its initial value sets how many threads can proceed concurrently before others block.- Use
sc_mutexfor a single exclusive resource; usesc_semaphorefor a pool of N identical resources. - Both
lock()andwait()are blocking — only legal insideSC_THREAD. Usetrylock()andtrywait()insideSC_METHOD. - Always match every
lock()with anunlock(), and everywait()with apost(). An unmatched blocking call drains the semaphore and eventually deadlocks the simulation. - Keep critical sections short. Holding a mutex across a long
wait()blocks all competing threads for the entire duration.
Part of the SystemC Foundations guide Browse all SystemC guides →