Semaphore Operations sem_wait, sem_post, sem_trywait, sem_timedwait

 

Semaphore Operations
Chapter 53 – Part 3 of 6 | sem_wait, sem_post, sem_trywait, sem_timedwait

Overview of Semaphore Operations

POSIX provides four functions to operate on semaphore values. All work on both named and unnamed semaphores. They all take a pointer to the semaphore (sem_t *).

  • sem_wait() — blocking decrement (P operation)
  • sem_trywait() — non-blocking decrement, returns EAGAIN if value is 0
  • sem_timedwait() — blocking decrement with a timeout deadline
  • sem_post() — increment (V operation)

sem_wait() – Blocking Decrement

#include <semaphore.h>

int sem_wait(sem_t *sem);
/* Returns: 0 on success, -1 on error (errno = EINTR if interrupted) */

sem_wait() performs the “wait” (P) operation:

  1. If the semaphore value is greater than 0: decrement it by 1 and return immediately.
  2. If the semaphore value is 0: the caller blocks (sleeps) until another thread or process calls sem_post().

sem_wait() Decision Flow
sem_wait() called
value > 0 ?
YES →
Decrement value by 1
Return 0 (success)
NO →
Block (sleep)
Wait for sem_post()
Note: sem_wait() can be interrupted by a signal. In that case it returns -1 with errno = EINTR. Always check the return value and retry on EINTR.

Handling EINTR in sem_wait()

/* Robust wrapper that retries on signal interruption */
int sem_wait_robust(sem_t *sem)
{
    while (sem_wait(sem) == -1) {
        if (errno != EINTR) {
            perror("sem_wait");
            return -1;        /* real error */
        }
        /* EINTR: retry */
    }
    return 0;
}

sem_trywait() – Non-Blocking Decrement

#include <semaphore.h>

int sem_trywait(sem_t *sem);
/* Returns: 0 on success, -1 on error
   errno = EAGAIN if value is 0 (would have blocked) */

sem_trywait() is the non-blocking variant. If the value is 0, it returns immediately with errno = EAGAIN instead of blocking. Use it when you want to attempt acquisition but do something else if the semaphore is not available.

#include <stdio.h>
#include <errno.h>
#include <semaphore.h>

void try_acquire(sem_t *sem)
{
    if (sem_trywait(sem) == 0) {
        printf("Acquired! Doing protected work.\n");
        /* ... critical section ... */
        sem_post(sem);
    } else {
        if (errno == EAGAIN) {
            printf("Semaphore busy, doing other work.\n");
        } else {
            perror("sem_trywait");
        }
    }
}

sem_timedwait() – Timed Blocking Decrement

#include <semaphore.h>
#include <time.h>

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
/* Returns: 0 on success, -1 on error
   errno = ETIMEDOUT if deadline reached without decrement */

sem_timedwait() blocks until either the semaphore can be decremented OR an absolute deadline (in CLOCK_REALTIME time) is reached.

⚠ Critical: abs_timeout is ABSOLUTE, not relative

The timeout is an absolute wall-clock time (seconds since the Unix epoch), not a duration. You must compute it yourself using clock_gettime(CLOCK_REALTIME, ...) and adding your desired timeout.

Example: Wait with 5-second timeout

#include <stdio.h>
#include <errno.h>
#include <time.h>
#include <semaphore.h>

int wait_with_timeout(sem_t *sem, int timeout_seconds)
{
    struct timespec deadline;

    /* Step 1: get current absolute time */
    if (clock_gettime(CLOCK_REALTIME, &deadline) == -1) {
        perror("clock_gettime");
        return -1;
    }

    /* Step 2: add relative timeout to get absolute deadline */
    deadline.tv_sec += timeout_seconds;

    /* Step 3: call sem_timedwait */
    while (sem_timedwait(sem, &deadline) == -1) {
        if (errno == EINTR)
            continue;          /* interrupted by signal, retry */
        if (errno == ETIMEDOUT) {
            printf("Timeout! Semaphore not available within %d seconds.\n",
                   timeout_seconds);
            return -1;
        }
        perror("sem_timedwait");
        return -1;
    }

    return 0;   /* success */
}

int main(void)
{
    sem_t sem;
    sem_init(&sem, 0, 0);    /* starts at 0, so wait will timeout */

    if (wait_with_timeout(&sem, 3) == 0)
        printf("Got semaphore.\n");
    else
        printf("Did not get semaphore.\n");

    sem_destroy(&sem);
    return 0;
}

sem_post() – Increment (Release)

#include <semaphore.h>

int sem_post(sem_t *sem);
/* Returns: 0 on success, -1 on error */

sem_post() increments the semaphore value by 1. If any threads or processes are blocked in sem_wait() on this semaphore, exactly one of them is woken up (which one is unspecified — up to the scheduler).

Key property: sem_post() is async-signal-safe

sem_post() is one of the few synchronization functions that can safely be called from a signal handler. This makes semaphores useful for signaling from a signal handler to a waiting thread.

sem_post() in a Signal Handler

#include <stdio.h>
#include <signal.h>
#include <semaphore.h>

sem_t sig_sem;

void sigint_handler(int sig)
{
    /* Safe to call sem_post from signal handler */
    sem_post(&sig_sem);
}

int main(void)
{
    sem_init(&sig_sem, 0, 0);
    signal(SIGINT, sigint_handler);

    printf("Press Ctrl+C to continue...\n");
    sem_wait(&sig_sem);       /* block until Ctrl+C */
    printf("Signal received, continuing.\n");

    sem_destroy(&sig_sem);
    return 0;
}

Operations Summary

Function Value = 0 ? Value > 0 ? Signal-Safe?
sem_wait() Blocks until post Decrements, returns 0 No
sem_trywait() Returns EAGAIN immediately Decrements, returns 0 No
sem_timedwait() Blocks until post or deadline Decrements, returns 0 No
sem_post() Increments to 1, wakes waiter Increments by 1 YES

Full Example: Producer-Consumer with Semaphores

Classic producer-consumer using three semaphores: one to track empty slots, one to track filled slots, and one mutex to protect the buffer.

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

#define BUFFER_SIZE 5
#define ITEMS       10

int buffer[BUFFER_SIZE];
int in_pos  = 0;
int out_pos = 0;

sem_t empty_slots;   /* tracks empty spaces in buffer */
sem_t filled_slots;  /* tracks filled spaces in buffer */
sem_t mutex;         /* protects buffer access */

void *producer(void *arg)
{
    for (int i = 0; i < ITEMS; i++) {
        sem_wait(&empty_slots);   /* wait for an empty slot */
        sem_wait(&mutex);         /* lock buffer */

        buffer[in_pos] = i;
        in_pos = (in_pos + 1) % BUFFER_SIZE;
        printf("Produced: %d\n", i);

        sem_post(&mutex);         /* unlock buffer */
        sem_post(&filled_slots);  /* signal a filled slot */
    }
    return NULL;
}

void *consumer(void *arg)
{
    for (int i = 0; i < ITEMS; i++) {
        sem_wait(&filled_slots);  /* wait for a filled slot */
        sem_wait(&mutex);         /* lock buffer */

        int val = buffer[out_pos];
        out_pos = (out_pos + 1) % BUFFER_SIZE;
        printf("  Consumed: %d\n", val);

        sem_post(&mutex);         /* unlock buffer */
        sem_post(&empty_slots);   /* signal an empty slot */
    }
    return NULL;
}

int main(void)
{
    pthread_t prod_tid, cons_tid;

    sem_init(&empty_slots, 0, BUFFER_SIZE); /* all slots empty initially */
    sem_init(&filled_slots, 0, 0);          /* no filled slots */
    sem_init(&mutex, 0, 1);                 /* binary mutex */

    pthread_create(&prod_tid, NULL, producer, NULL);
    pthread_create(&cons_tid, NULL, consumer, NULL);

    pthread_join(prod_tid, NULL);
    pthread_join(cons_tid, NULL);

    sem_destroy(&empty_slots);
    sem_destroy(&filled_slots);
    sem_destroy(&mutex);
    return 0;
}
gcc producer_consumer.c -o prod_cons -lpthread
./prod_cons

Interview Questions – Semaphore Operations

Q1. What is the difference between sem_wait() and sem_trywait()?
sem_wait() blocks the calling thread if the semaphore value is 0, waiting until sem_post() is called. sem_trywait() never blocks — if the value is 0 it immediately returns -1 with errno = EAGAIN, allowing the caller to do other work.
Q2. What does sem_timedwait() use as its timeout argument — absolute or relative time?
Absolute time based on CLOCK_REALTIME (seconds since the Unix epoch). You must first call clock_gettime(CLOCK_REALTIME, &ts) to get the current time, then add your desired timeout in seconds to ts.tv_sec before passing to sem_timedwait().
Q3. Why is sem_post() async-signal-safe but sem_wait() is not?
sem_post() is a simple atomic increment. It does not allocate memory, acquire locks, or call into complex library code, so it satisfies the async-signal-safe requirements. sem_wait() may need to sleep and interact with the kernel scheduler, which involves non-reentrant library internals.
Q4. sem_wait() returns -1 with errno = EINTR. What should you do?
EINTR means the system call was interrupted by a signal before the semaphore could be acquired. The correct response is to check for EINTR and retry sem_wait() in a loop. This is standard practice for any interruptible blocking system call in Linux.
Q5. In the producer-consumer problem, why are three semaphores used instead of one?
One semaphore (empty_slots) counts available empty buffer positions — the producer waits on it. A second (filled_slots) counts filled positions — the consumer waits on it. A third (mutex) acts as a binary semaphore protecting the buffer data structure itself. Using two semaphores allows producer and consumer to operate concurrently as long as the buffer is neither full nor empty.
Q6. What error does sem_timedwait() return when the deadline expires?
It returns -1 with errno set to ETIMEDOUT. The semaphore value is not changed in this case — the operation simply did not succeed within the given time.

Leave a Reply

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