Prev: condition-variables Next: common-concurrency-problems
Semaphores are a generic synchronization primitive that can be used as locks and condition variables.
POSIX has semaphores in the standard in semaphore.h
.
#include <semaphore.h>
;
sem_t s(&s, 0, 1); sem_init
Semaphores are initialized with sem_init
, which takes a
semaphore, an enum which indicates if the semaphore is shared between
threads in the same process, and the count, which is 1
.
Wait and post are defined like so:
sem_wait
decrements the value of the semaphore by one.
If it is still positive, it returns there. If not, it tells the calling
thread to sleep.
sem_post
increments the value of the semaphore by one.
If one or more thread is waiting, it wakes up one of them.
int sem_wait(sem_t *s) {
// decrement the value of semaphore s by one
// wait if value of semaphore s is negative
}
int sem_post(sem_t *s) {
// increment the value of semaphore s by one
// if there are one or more threads waiting, wake one
}
Semaphores set to a value of 1
are basically mutexes,
but less efficient (due to having to signal).
To emulate a condition variable, Semaphores can also be used, but
with their value set to 0
.
;
sem_t s
void *child(void *arg) {
("child\n");
printf(&s); // signal here: child is done
sem_postreturn NULL;
}
int main(int argc, char *argv[]) {
(&s, 0, 0);
sem_init("parent: begin\n");
printf;
pthread_t c(&c, NULL, child, NULL);
Pthread_create(&s); // wait here for child
sem_wait("parent: end\n");
printfreturn 0;
}
This means that the semaphore starts at 0, and then the parent does what it needs to, decrements the semaphore, and is put to sleep. Then the child will post, which wakes the parent.
This can be solved with a 2 semaphores and a mutex. The semaphores signal for empty and full, and the mutex is used to guard the critical section.
void *producer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
(&empty); // Line P1
sem_wait(&mutex); // Line P1.5 (MUTEX HERE)
sem_wait(i); // Line P2
put(&mutex); // Line P2.5 (AND HERE)
sem_post(&full); // Line P3
sem_post}
}
void *consumer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
(&full); // Line C1
sem_wait(&mutex); // Line C1.5 (MUTEX HERE)
sem_waitint tmp = get(); // Line C2
(&mutex); // Line C2.5 (AND HERE)
sem_post(&empty); // Line C3
sem_post("%d\n", tmp);
printf}
}
A Common pattern that occurs is for locking on writers, but not on readers, due to writers mutating data and readers not requiring a lock.
To do this, we use semaphores – one that indicates if it is safe to write, and another for the critical section.
typedef struct _rwlock_t {
; // binary semaphore (basic lock)
sem_t lock; // allow ONE writer/MANY readers
sem_t writelockint readers; // #readers in critical section
} rwlock_t;
void rwlock_init(rwlock_t *rw) {
->readers = 0;
rw(&rw->lock, 0, 1);
sem_init(&rw->writelock, 0, 1);
sem_init}
void rwlock_acquire_readlock(rwlock_t *rw) {
(&rw->lock);
sem_wait->readers++;
rwif (rw->readers == 1) // first reader gets writelock
(&rw->writelock);
sem_wait(&rw->lock);
sem_post}
void rwlock_release_readlock(rwlock_t *rw) {
(&rw->lock);
sem_wait->readers--;
rwif (rw->readers == 0) // last reader lets it go
(&rw->writelock);
sem_post(&rw->lock);
sem_post}
void rwlock_acquire_writelock(rwlock_t *rw) { sem_wait(&rw->writelock); }
void rwlock_release_writelock(rwlock_t *rw) { sem_post(&rw->writelock); }
The Dining philosophers problem has five philosophers at a table, who need to grab two forks next to them to eat. They think, grab forks, eat, then put their forks down. The challenge is to make sure that there is no deadlock, no philosopher starves, and throughput is high.
A broken solution involves locking the forks, where each philosopher grabs the fork to their left and then their right. If each philosopher grabs the fork to their left, then tries to grab the fork to their right, they enter deadlock, since no one can make progress.
Dijkstra’s solution is fairly simple. Make one of the philosophers right-handed – they grab a different fork, so they break the cycle of deadlock. Another way is cooperative – a philosopher who grabbed a fork may put it down. To ensure fairness, there might be an epoch counter and the philosophers who put down the fork in one round might not in the next round.
Semaphores are also useful for throttling – each thread that wants to do computation calls the semaphore. This is good for rate limiting (no more than X threads can be in the critical section at once).
Semaphores can be implemented using condition variables and locks:
typedef struct __Zem_t {
int value;
;
pthread_cond_t cond;
pthread_mutex_t lock} Zem_t;
// only one thread can call this
void Zem_init(Zem_t *s, int value) {
->value = value;
s(&s->cond);
Cond_init(&s->lock);
Mutex_init}
void Zem_wait(Zem_t *s) {
(&s->lock);
Mutex_lockwhile (s->value <= 0)
(&s->cond, &s->lock);
Cond_wait->value--;
s(&s->lock);
Mutex_unlock}
void Zem_post(Zem_t *s) {
(&s->lock);
Mutex_lock->value++;
s(&s->cond);
Cond_signal(&s->lock);
Mutex_unlock}
Prev: condition-variables Next: common-concurrency-problems