9 KiB
Semaphores — SignalSemaphore, ObtainSemaphore, Shared/Exclusive
Overview
Semaphores are the AmigaOS mechanism for mutual exclusion and shared-read access to resources. Unlike Forbid() (which blocks all scheduling), semaphores allow other tasks to run while waiting — the waiting task simply sleeps until the resource is available. They are the correct synchronization primitive for anything that takes more than a few microseconds.
Architecture
stateDiagram-v2
[*] --> FREE : InitSemaphore()
FREE --> EXCLUSIVE : ObtainSemaphore() (by Task A)
FREE --> SHARED : ObtainSemaphoreShared() (by Task A)
SHARED --> SHARED : ObtainSemaphoreShared() (by Task B — multiple OK)
SHARED --> FREE : All shared holders release
EXCLUSIVE --> NESTED : ObtainSemaphore() (by same Task A — reentrant)
NESTED --> EXCLUSIVE : ReleaseSemaphore() (decrement nest count)
EXCLUSIVE --> FREE : ReleaseSemaphore() (nest count reaches 0)
EXCLUSIVE --> BLOCKED : ObtainSemaphore() by Task B → Task B sleeps
BLOCKED --> EXCLUSIVE : Task A releases → Task B wakes
Shared vs Exclusive
| Mode | Multiple holders? | Use case |
|---|---|---|
Exclusive (ObtainSemaphore) |
No — only one task at a time | Writing/modifying shared data |
Shared (ObtainSemaphoreShared) |
Yes — multiple readers allowed | Read-only access to shared data |
When a task requests exclusive access while shared holders exist, it blocks until ALL shared holders release. When a task requests shared access while an exclusive holder exists, it blocks until the exclusive holder releases.
struct SignalSemaphore
/* exec/semaphores.h — NDK39 */
struct SignalSemaphore {
struct Node ss_Link; /* ln_Type = NT_SIGNALSEM */
/* ln_Name = semaphore name (public) */
WORD ss_NestCount; /* how many times THIS task has obtained it */
struct MinList ss_WaitQueue;/* tasks waiting for access */
struct SemaphoreRequest ss_MultipleLink; /* shared-reader management */
struct Task *ss_Owner; /* task holding exclusive lock (or NULL) */
WORD ss_QueueCount; /* internal waiter tracking */
};
| Field | Description |
|---|---|
ss_Link.ln_Name |
Name string — set for public (findable) semaphores |
ss_NestCount |
How many times the current owner has obtained it (reentrant) |
ss_WaitQueue |
Queue of tasks waiting for access |
ss_Owner |
Task holding exclusive lock, or NULL if free/shared-only |
ss_QueueCount |
Internal — tracks waiting tasks and shared readers |
Initializing a Semaphore
/* Stack or heap — always initialize before use: */
struct SignalSemaphore sem;
InitSemaphore(&sem); /* LVO -558 */
/* Public (named) semaphore — findable by other tasks: */
sem.ss_Link.ln_Name = "myapp.lock";
sem.ss_Link.ln_Pri = 0;
AddSemaphore(&sem); /* LVO -564 — adds to SysBase→SemaphoreList */
/* Find from another task: */
Forbid();
struct SignalSemaphore *found = FindSemaphore("myapp.lock"); /* LVO -576 */
Permit();
/* Cleanup: */
RemSemaphore(&sem); /* LVO -570 */
Exclusive (Write) Lock
/* Block until this task holds the semaphore exclusively: */
ObtainSemaphore(&sem); /* LVO -534 */
/* --- critical section: only one task in here at a time --- */
ModifySharedData();
ReleaseSemaphore(&sem); /* LVO -546 */
Non-Blocking Try
/* Returns TRUE if obtained, FALSE if someone else holds it: */
if (AttemptSemaphore(&sem)) /* LVO -540 */
{
/* Got exclusive access */
ModifySharedData();
ReleaseSemaphore(&sem);
}
else
{
/* Resource busy — do something else or retry later */
}
Shared-to-Exclusive Upgrade (OS 3.0+)
/* AttemptSemaphoreShared — try shared lock without blocking */
if (AttemptSemaphoreShared(&sem)) /* LVO -774 */
{
/* Got shared access */
ReadSharedData();
ReleaseSemaphore(&sem);
}
Shared (Read) Lock
Multiple tasks may hold a shared lock simultaneously. An exclusive lock request blocks until all shared holders release.
ObtainSemaphoreShared(&sem); /* LVO -768 */
/* --- read-only access: multiple tasks may be here at once --- */
result = ReadSharedData();
ReleaseSemaphore(&sem); /* Same release for both modes */
Nesting (Reentrancy)
Semaphores are reentrant — the same task can call ObtainSemaphore multiple times without deadlocking itself:
ObtainSemaphore(&sem); /* NestCount = 1, Owner = thisTask */
ObtainSemaphore(&sem); /* NestCount = 2 — safe, same task */
ObtainSemaphore(&sem); /* NestCount = 3 */
ReleaseSemaphore(&sem); /* NestCount = 2 */
ReleaseSemaphore(&sem); /* NestCount = 1 */
ReleaseSemaphore(&sem); /* NestCount = 0 — fully released, waiters wake */
This is essential for recursive functions or library code that may be called from contexts that already hold the lock.
Obtaining Multiple Semaphores
To avoid deadlocks when you need multiple semaphores, use ObtainSemaphoreList:
/* Build a list of semaphores to obtain atomically: */
struct SemaphoreRequest reqA, reqB;
struct List semList;
NewList(&semList);
reqA.sr_Semaphore = &semA;
reqB.sr_Semaphore = &semB;
AddTail(&semList, &reqA.sr_Link);
AddTail(&semList, &reqB.sr_Link);
ObtainSemaphoreList(&semList); /* LVO -582 */
/* Both semaphores held — no deadlock risk */
ReleaseSemaphore(&semA);
ReleaseSemaphore(&semB);
Semaphore vs Forbid vs Disable
| Mechanism | Blocks | Other tasks run? | Interrupt safe? | Cost | Max duration |
|---|---|---|---|---|---|
Forbid() |
All task switching | ❌ No | ✅ Ints still run | Very low | ~100 ms |
Disable() |
All ints + tasks | ❌ No | ✅ (is the lock) | Lowest | ~250 µs |
ObtainSemaphore() |
Only contending tasks | ✅ Yes | ❌ Not from IRQ | Medium | Unlimited |
ObtainSemaphoreShared() |
Only if exclusive held | ✅ Yes | ❌ Not from IRQ | Medium | Unlimited |
Decision Guide
graph TD
Q1{"In interrupt<br/>context?"} -->|Yes| DISABLE["Use Disable()"]
Q1 -->|No| Q2{"Duration<br/>< 10 µs?"}
Q2 -->|Yes| FORBID["Use Forbid()"]
Q2 -->|No| Q3{"Multiple<br/>readers OK?"}
Q3 -->|Yes| SHARED["ObtainSemaphoreShared()"]
Q3 -->|No| EXCL["ObtainSemaphore()"]
style DISABLE fill:#ffcdd2,stroke:#e53935,color:#333
style FORBID fill:#fff3e0,stroke:#ff9800,color:#333
style SHARED fill:#e8f5e9,stroke:#4caf50,color:#333
style EXCL fill:#e8f4fd,stroke:#2196f3,color:#333
Pitfalls
1. Deadlock (Lock Ordering)
/* Task A */ /* Task B */
ObtainSemaphore(&semX); ObtainSemaphore(&semY);
ObtainSemaphore(&semY); /*!*/ ObtainSemaphore(&semX); /*!*/
/* Task A waits for Y Task B waits for X
→ DEADLOCK — both tasks sleep forever */
Solution: Always obtain semaphores in the same global order (alphabetical, by address, etc.).
2. Priority Inversion
/* Low-pri task holds semaphore, medium-pri task runs,
high-pri task waits for semaphore → high-pri starves.
AmigaOS has NO priority inheritance. */
Solution: Keep critical sections short; don't hold semaphores across I/O.
3. ObtainSemaphore from Interrupt Context
/* CRASH — ObtainSemaphore may Wait(), which is illegal from interrupts */
void __interrupt MyHandler(void)
{
ObtainSemaphore(&sem); /* DEADLOCK or crash */
}
Solution: Use Disable()/Enable() for interrupt-level synchronization, or use AttemptSemaphore() (non-blocking) and skip if busy.
4. Forgetting to Release
/* BUG — semaphore held forever */
ObtainSemaphore(&sem);
if (error) return; /* Returns without releasing! */
ReleaseSemaphore(&sem);
/* CORRECT — use cleanup pattern */
ObtainSemaphore(&sem);
if (error) goto cleanup;
/* ... work ... */
cleanup:
ReleaseSemaphore(&sem);
5. Shared/Exclusive Mismatch
/* Not a bug, but confusing — ReleaseSemaphore works for both modes */
ObtainSemaphoreShared(&sem);
ReleaseSemaphore(&sem); /* Correct — same function for both */
Best Practices
- Use semaphores instead of
Forbid()for anything > ~10 µs - Prefer shared locks for read-only access — maximizes parallelism
- Keep critical sections short — don't do I/O while holding a semaphore
- Use consistent lock ordering to prevent deadlocks
- Use
AttemptSemaphore()for non-blocking try-lock patterns - Always release on every code path — use goto-cleanup pattern
- Never call from interrupts — use
Disable()orAttemptSemaphore()instead - Use
ObtainSemaphoreList()when you need multiple semaphores atomically
References
- NDK39:
exec/semaphores.h - ADCD 2.1:
InitSemaphore,ObtainSemaphore,ObtainSemaphoreShared,ReleaseSemaphore,AttemptSemaphore,ObtainSemaphoreList - See also: Multitasking — priority inversion and synchronization strategies
- Amiga ROM Kernel Reference Manual: Exec — semaphores chapter