Semaphore Comparisons

 

Semaphore Comparisons
POSIX vs System V • POSIX vs Mutexes — File 4 of 5

Why Compare Synchronization Techniques?

Linux offers multiple ways to synchronize concurrent processes and threads: System V semaphores (the older POSIX.1 approach), POSIX semaphores (the modern approach), and Pthreads mutexes (for thread-level synchronization). Understanding when to use each — and why — is an important part of systems programming expertise.

Section 53.5 of TLPI directly compares these three approaches with concrete tradeoffs. This file walks through each comparison in depth with code context where helpful.

Section 53.5 (Part 1): POSIX Named Semaphores vs System V Semaphores

Both can synchronize unrelated processes. The difference is in API design, initialization, and performance.

POSIX Semaphore Advantages over System V
1. Simpler Interface

POSIX semaphore API uses individual sem_t objects with simple functions. System V uses semaphore sets (semget, semctl, semop), requires IPC keys, and has complex data structures. POSIX achieves the same functional power with far fewer concepts.

2. No Initialization Race (Named Semaphores)

System V semaphores have a notorious race condition: a process must first create the semaphore set, then separately initialize it with semctl(SETVAL). Between these two steps, another process might start using an uninitialized semaphore. POSIX sem_open() creates AND initializes atomically, eliminating this problem entirely.

3. Easier Embedding in Dynamic Objects

A POSIX unnamed semaphore can be embedded directly inside a struct or a dynamically allocated memory object. You cannot do this with System V semaphores, which exist only as kernel objects accessed via an integer ID (semid).

/* Embedding a semaphore directly in a struct */
struct shared_data {
    sem_t lock;       /* Semaphore lives inside the struct */
    int   counter;
    char  buffer[256];
};

/* mmap a region, cast to struct, init the embedded semaphore */
struct shared_data *shm = mmap(...);
sem_init(&shm->lock, 1, 1);  /* Process-shared, value=1 */
4. Better Performance Under Low Contention

This is the most significant practical advantage. POSIX semaphore operations (especially on Linux using futexes) only require a system call when actual blocking is needed. System V semaphore operations always require a system call regardless of contention. The author of TLPI measured a performance difference of more than an order of magnitude (10x) in favor of POSIX semaphores under low contention.

Why the Performance Gap?
POSIX Semaphore (futex-based)
sem_wait() with value > 0:
✓ Atomic decrement in user-space
✓ No system call needed
sem_wait() with value == 0:
! futex(WAIT) syscall — only then
System V Semaphore
semop() — every single call:
✗ Always enters kernel
✗ System call overhead every time
✗ Even when no blocking needed
5. POSIX IPC General Advantages

POSIX IPC objects are reference-counted, making it easy to determine when an object can be safely deleted. The POSIX API follows the familiar file model (names, open/close/unlink), which is more intuitive. System V uses numeric keys and integer IDs with no connection to the filesystem model.

POSIX Semaphore Disadvantages vs System V
1. Less Portable (Historically)

On Linux, named POSIX semaphores were supported only since kernel 2.6. System V semaphores have been available much longer across all UNIX variants. Today (Linux 2.6+), this is rarely a concern, but legacy codebases on older platforms may still need System V.

2. No Equivalent of the “Undo” Feature

System V semaphores have a SEM_UNDO flag. When a process terminates abnormally, the kernel automatically reverses any semaphore operations that were performed with SEM_UNDO. POSIX semaphores have no such feature — if a process crashes while holding a POSIX semaphore, the semaphore stays decremented and other processes may deadlock. However, the SEM_UNDO feature has limitations and is not always reliable or useful.

Side-by-Side Comparison: POSIX Named vs System V Semaphores
Feature POSIX Named Semaphore System V Semaphore
Create/Open sem_open("/name", O_CREAT, mode, value) semget(key, nsems, IPC_CREAT | perms)
Initialize Atomic with open (O_CREAT + value arg) Separate semctl(SETVAL) — race window exists
Operate sem_wait(), sem_post() semop() with sembuf array
Delete sem_unlink("/name") semctl(semid, 0, IPC_RMID)
Granularity One semaphore per object Set of semaphores per semid
Performance Futex-based: syscall only on contention (10x+ faster under low contention) Always syscall — slower uniformly
Undo on crash Not available SEM_UNDO flag available
Reference counted Yes — kernel tracks open count No — must manually track users
API complexity Simple, file-like Complex: key generation, semun union, multiple ctl commands

Section 53.5 (Part 2): POSIX Semaphores vs Pthreads Mutexes

Both can synchronize threads within the same process and have similar performance. But they have different characteristics that make each better suited for different problems.

Why Mutexes are Usually Preferable for Thread Sync
Ownership Enforces Good Design

A mutex has a strict ownership rule: only the thread that locked the mutex can unlock it. This enforces a clear lock/unlock pattern within the same scope and prevents accidental unlock by unrelated code.

A semaphore has no ownership. Thread A can decrement it and Thread B can increment it. While this flexibility is sometimes useful, it can lead to poorly structured synchronization designs where it is unclear which code is responsible for releasing a resource.

This is why semaphores are sometimes called the “gotos of concurrent programming” — they work, but can make code hard to reason about.

Code Comparison: Mutex vs Binary Semaphore for Critical Section
With Mutex (Preferred for threads)
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void thread_func(void) {
    pthread_mutex_lock(&lock);
    /* Critical section */
    shared_counter++;
    pthread_mutex_unlock(&lock);
    /* Clear ownership: only locker unlocks */
}
With Semaphore (Binary)
sem_t sem;
sem_init(&sem, 0, 1);

void thread_func(void) {
    sem_wait(&sem);
    /* Critical section */
    shared_counter++;
    sem_post(&sem);
    /* No ownership: ANY thread could post */
}
One Case Where Semaphores Beat Mutexes: Signal Handlers

sem_post() is async-signal-safe. This means it can safely be called from within a signal handler. Pthreads mutex functions are NOT async-signal-safe — calling pthread_mutex_unlock() from a signal handler results in undefined behavior.

This matters when you need a signal handler to wake up a blocking thread. The recommended pattern:

/*
 * Pattern: signal handler posts a semaphore to wake a waiting thread.
 * Works because sem_post() is async-signal-safe.
 */
#include <semaphore.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

static sem_t sig_sem;

/* Signal handler: just post the semaphore */
static void sigint_handler(int sig) {
    sem_post(&sig_sem);   /* async-signal-safe: OK */
}

/* Thread that waits for a signal via semaphore */
static void *waiter_thread(void *arg) {
    printf("Thread: waiting for signal...\n");
    sem_wait(&sig_sem);
    printf("Thread: received signal, cleaning up.\n");
    return NULL;
}

int main(void) {
    pthread_t tid;
    struct sigaction sa;

    sem_init(&sig_sem, 0, 0); /* start at 0: thread will block */

    sa.sa_handler = sigint_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);

    pthread_create(&tid, NULL, waiter_thread, NULL);
    pthread_join(tid, NULL);

    sem_destroy(&sig_sem);
    return 0;
}
/* Press Ctrl+C to trigger SIGINT and wake the thread */
/* Compile: gcc -o sig_sem sig_sem.c -lpthread */

Note from TLPI: It is usually more elegant to handle async signals using sigwaitinfo() rather than signal handlers (see Section 33.2.4). This makes the semaphore advantage over mutexes in signal handling less commonly needed in practice.

When to Use What: Decision Summary
Use Case Mutex POSIX Semaphore System V Semaphore
Protect shared data between threads Best OK Poor
Sync between unrelated processes N/A Best (named) OK
Signal from signal handler to thread Cannot Best (sem_post is async-signal-safe) Cannot (semop not async-signal-safe)
Pool of N resources (counting) No (binary only) Best OK
Producer-consumer signaling With condition variable Best (simpler) OK
High-contention locking Best Similar Similar
Auto-undo on process crash No No Yes (SEM_UNDO)
Performance under low contention Best (futex) Best (futex) Poor (always syscall)

Interview Questions — Semaphore Comparisons
Q1. Why are POSIX semaphores faster than System V semaphores under low contention?

POSIX semaphores on Linux are implemented using futexes (fast user-space mutexes). When the semaphore value allows an operation to proceed without blocking, the operation is completed with an atomic user-space instruction and no system call is made. System V semaphore operations (semop) always make a system call regardless of whether blocking is needed, causing kernel overhead every time.

Q2. What is the initialization race problem in System V semaphores and how does POSIX solve it?

In System V, creating a semaphore set (semget) and initializing it (semctl with SETVAL) are two separate steps. Another process could use the semaphore between these steps when it is in an undefined state. POSIX sem_open() with O_CREAT atomically creates AND sets the initial value in a single step, eliminating the window entirely.

Q3. Why are mutexes usually preferred over semaphores for protecting critical sections in multithreaded code?

Mutexes enforce ownership: only the thread that acquired the lock can release it. This enforces disciplined coding — the lock and unlock always appear in the same scope/function. Semaphores have no ownership, so any thread can increment them, which can lead to accidental double-release or mismatched acquire/release patterns — bugs that are hard to detect.

Q4. Why are semaphores called the “gotos of concurrent programming”?

Just as goto in sequential code can make control flow hard to follow, semaphore operations scattered across different threads can make synchronization flow hard to reason about. Because sem_post() and sem_wait() can be called by unrelated code in any order with no ownership constraint, it is easy to create synchronization patterns that are correct but difficult to understand, maintain, or audit.

Q5. What is SEM_UNDO in System V semaphores and why does POSIX not have it?

SEM_UNDO is a flag for semop() that asks the kernel to automatically reverse any semaphore operations when the process terminates. This prevents deadlock if a process crashes while holding a semaphore. POSIX semaphores have no equivalent. However, TLPI notes that SEM_UNDO has limitations (it doesn’t work well across fork, and has per-process adjustment limits), so in many real-world scenarios it is not as useful as it sounds.

Q6. Can you call sem_post() from inside a signal handler?

Yes. sem_post() is listed as async-signal-safe in POSIX (Table 21-1 in TLPI), meaning it is safe to call from a signal handler. This makes it possible to use a semaphore to wake up a blocked thread from a signal handler. Mutex functions (pthread_mutex_lock/unlock) are NOT async-signal-safe and cannot be used in signal handlers.

Next: Semaphore Limits and Complete Summary

Learn about SEM_NSEMS_MAX, SEM_VALUE_MAX, and review all concepts with a final Q&A.

← Unnamed Semaphores File 5: Limits & Summary →

Leave a Reply

Your email address will not be published. Required fields are marked *