OS: Semaphores
Semaphores extend the mutex idea. A semaphore is an atomic counter with two operations:
- sem_wait(&s): decrements the counter. If it's 0, the thread waits until someone else increments.
- sem_post(&s): increments the counter (and wakes up one blocked thread, if any).
In C, POSIX gives us the <semaphore.h> library. A semaphore is declared with the sem_t type and initialized with sem_init(&s, 0, initial_value). The second argument is 0 when the semaphore is shared between threads of the same process (our case); 1 would mean shared between different processes.
When we initialize the semaphore with 1, it behaves like a mutex: one thread enters (the counter goes to 0), and the others wait until it leaves and increments back.
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
sem_t sem;
int counter = 0;
void *increment(void *arg) {
for (int i = 0; i < 100000; i++) {
sem_wait(&sem);
counter++;
sem_post(&sem);
}
return NULL;
}
int main(void) {
sem_init(&sem, 0, 1);
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Counter: %d\n", counter);
sem_destroy(&sem);
return 0;
}
The result is always 200000. The code is equivalent to the mutex version from the previous lesson. Only the mechanism changed. Why use a semaphore instead of a mutex? Because the semaphore's counter can exceed 1, and that makes it far more flexible.
A counting semaphore (initialized to N > 1) models a number of shared resources. Picture the computing centre with 5 printers: five jobs can run at once, and the sixth one waits until one of them frees up.
sem_t printers;
sem_init(&printers, 0, 5);
// in each thread:
sem_wait(&printers); // grab a printer (counter--)
// ... run the job
sem_post(&printers); // release the printer (counter++)
When five threads have called sem_wait, the counter is 0. The sixth thread blocks on sem_wait until one of the running jobs finishes and calls sem_post.
A mutex couldn't do this: a mutex is always binary (held or free). A counting semaphore caps how many go in at once, not just one.
Don't forget sem_destroy(&sem) at the end: it releases internal resources. And sem_init's third argument is the counter's starting value: 0, 1, or N.
Mission: The Printer Queue
The computing centre has 2 printers available. Four operators submit jobs simultaneously, but at any given moment at most 2 can be printing; the others queue politely.
- Create a semaphore initialized to 2.
- Start 4 threads, each with its ID (1, 2, 3, 4) passed as an argument.
- Each thread:
- Waits on the semaphore (
sem_wait). - Prints
Thread K printing. - Prints
Thread K done. - Releases the semaphore (
sem_post).
- Waits on the semaphore (
- The main thread waits for all 4 threads with
pthread_join, then printsAll jobs finished.
Example
The order of the printing/done lines may vary. The rule is: at any moment, at most 2 threads are between printing and done.
Thread 1 printing
Thread 2 printing
Thread 1 done
Thread 3 printing
Thread 2 done
Thread 4 printing
Thread 3 done
Thread 4 done
All jobs finished
The final message always appears last, because the main thread waits with pthread_join.
Use sem_t, sem_init, sem_wait, sem_post, sem_destroy from <semaphore.h>, plus pthread_create and pthread_join from <pthread.h>.