The original proposed synchronisation primitive for all things synchronisation. Can function as a lock as well as as a condition variable, whatever that is
A semaphore is an object with an integer value that we can sem_wait() or sem_signal() for. It’s initial value is quite important so it must be initted well
int sem_wait(sem_t *s) { /* * decrement the value of semaphore s by 1 * wait if value of semaphore is negative */}
int sem_signal(sem_t *s) { /* * increment the value of semaphore by 1 * if there are one or more threads waiting, wake one */}
Semaphore Invariant
The value of the semaphore, when negative, is equal to the number of waiting threads
Semaphores as Locks
Here the value of the semaphore is usually just between two states, locked and unlocked
When we want something to occur AFTER another thing has happened, we start in a “locked” state.
sem_t *s;void *thing1(void* arg) { (void*)arg; printf("child: heheheheheheh\n"): sem_signal(s); // signal end of thing1 return NULL;}int main(void) { sem_init(s, NULL, 0); // init locked sem pthread_t t; printf("parent: begin\n"); pthread_create(&c, NULL, thing1, NULL); sem_wait(s); // wait for thing1 to finish printf("parent: end\n"); // do thing 2 return 0;}
Initial Semaphore Values
We mentioned the invariant above about how the negative value indicates the number of waiting processes. Similar the positive value indicates the number of immediately available resources after initialization.
A value of 0 indicates there are no resources to give away yet. 1 and higher say these many threads can start running simultaneously
Classical Problems
Produce/Consumer(Bounded Buffer Problem)
Consider async events. One thread may gather and add them to a queue, while another pops them and processes each event. The first thread is the producer and the second, the consumer.
Queue/Buffer operations are critical and must be mutually exclusive
If a consumer thread arrives while the buffer is empty, it blocks until the a producer adds a new item
we create three semaphores
full to keep track of when items are available
empty for when the buffer is empty
mutex for mutual exclusion
sem_t full, empty, mutex;void *producer(void* arg) { for (;;) { sem_wait(&empty); sem_wait(&mutex); /* add item i to buffer */ sem_signal(&mutex); sem_signal(&full); }}void *consumer(void* arg) { while(1) { sem_wait(&full); sem_wait(&mutex); /* consume item */ sem_signal(&mutex); sem_signal(&empty); }}int main(void) { // ... sem_init(&empty, NULL, NO_ITEMS); // NO_ITEMS available sem_init(&full, NULL, 0); // 0 are full sem_init(&mutex, NULL, 1); // mutex is unlocked}
Mutual Exclusion and Deadlocks
A producer will not be running at the same time as a consumer, and vice versa, thanks to the empty and full semaphores. But in the event of multiple producers/consumers, so prevent race conditions when adding/consuming, we need the third mutex semaphore.
If it is not, then it goes to sleep and NEVER RELEASES THE MUTEX
the mutex is acquired by a sleeping process and nothing else can acquire it giving us a DEADLOCK
We fix this by reducing the scope of the lock
Designated Producer-Consumer Problem (unbounded buffer)
Consider a similar scenario, but now each item was produced for a designated consumer and must be consumer only by that consumer. We now mantain an array of semaphores each individually telling its designated consumer if it can consume or not;
void* consumer(int i) { sem_wait(&r_mutex[i]); for (;;) { sem_wait(&rw_mutex); /* consume item from buffer */ sem_signal(&rw_mutex); }}void* producer() { for (;;) { int i = rand() % N; sem_wait(&rw_mutex); /* produce item for i */ sem_signal(&r_mutex[i]); sem_signal(&rw_mutex); }}
extending this for a bounded buffer shouldn’t be hard i don’t think
The Reader-Writer Problem
For the different requirements of different data structures. Consider a database
Any number of readers can be in a critical section simultaneously
Writers must have exclusive access to the critical section
This can be called categorical mutual exclusion
There are many versions of this problem, we are first going to look at the Reader-Priority version
Reader-Priority
We would need a few variables
a w_mutex: to ensure writers are mutually exclusive
a readers int to count the number of readers accessing the db
a r_mutex to protect the readers int
sem_t w_mutex;sem_t r_mutex;int readers = 0;void* writer(void* arg) { sem_wait(&w_mutex); /* write to db */ sem_signal(&w_mutex);}void* reader(void* arg) { sem_wait(&r_mutex); readers++; if (readers == 1) { /* first reader => wait for writers to free up */ sem_wait(&w_mutex); } sem_signal(&r_mutex); /* read from db */ sem_wait(&r_mutex); readers--; if (readers == 0) { /* last reader => wakeup writers now */ sem_signal(&w_mutex); } sem_signal(&r_mutex);}
Lightswitches
Patterns like can be called Lightswitches, where the first one in turns on the light, and the last one out turns it off
Writer Starvation
We noticed earlier that waiting on a semaphore, while already holding a semaphore could lead to potential deadlocks, but thankfully in this case we are safe, since the writer never touches r_mutex. However we have another, related problem. Our solution above favours readers, if a new reader were to come while a writer was waiting, it would be queued before the writer.
In such a scenario, writers could starve and potentially never run. To mitigate this we would need another semaphore to tell readers that a writer is waiting and stop letting new readers in.
If we operate under the assumption that semaphore queues are FIFO queues, we can do the following
sem_t r_mutex, w_mutex, q_mutex;int readers = 0;void* writer(void* arg) { sem_wait(&q_mutex); sem_wait(&w_mutex); sem_signal(&q_mutex); /* write to db */ sem_signal(&w_mutex);}
If a writer enters while a reader is reading, it will block on line 4 waiting for w_mutex, and in the process q_mutex will be locked as well. Preventing other readers from entering the queue
void* reader(void* arg) { sem_wait(&q_mutex); sem_wait(&r_mutex); readers++; if (readers == 1) { sem_wait(&w_mutex); } sem_signal(&q_mutex); sem_signal(&r_mutex); /* read from db */ sem_wait(&r_mutex); readers--; if (readers == 0) { sem_signal(&w_mutex); } sem_signal(&r_mutex);}
However, all this ensures is that one writer gets to proceed before the other readers. It still favours the readers to get ahead of writers in general. Secondly, here we assume that a semaphore waiting queue is FIFO, which may not be true and potentially varies from implementation to implementation.
Writer-Priority
We will now implement a lightswitch type design for writers as well
void *reader(void* arg) { sem_wait(&block_readers); // NEW LINE // wait for read to be unblocked wait(&r_mutex); readers++; if (readers == 1) { sem_wait(&block_writers); // first reader blocks writers } sem_signal(&r_mutex); sem_signal(&block_readers); // NEW LINE // /* read from db */ sem_wait(&r_mutex); readers--; if (readers == 0) { sem_signal(&block_writers); // last reader unblocks writers } sem_signal(&r_mutex);}void *writer(void* arg) { sem_wait(&w_mutex); writers++; if (writers == 1) { sem_wait(&block_readers); // first writer blocks readers } sem_signal(&w_mutex); sem_wait(&block_writers); // block other writers to enter CS /* write to db */ sem_signal(&block_writers); // unblock writers sem_wait(&w_mutex); writers--; if (writers == 0) { sem_signal(&block_readers); // last writer unblocks readers } sem_signal(&w_mutex);}
Now a writer can always block readers. This ofcourse leads to the issue that readers may starve now :(
Since the table is circular, if every nerd picked up the left fork at the same time, none of them have a right fork they can use and are forever waiting. This will lead to them starving and eventually dying.
Resolving Deadlocks
One way to avoid deadlock is to think about the conditions that make the deadlock possible, then change one of the conditions. The Dining Philosophers’ deadlock is fairly fragile
Broken Solution: 2
All of pick their left forks, and check to see if the right is available. Since the issue with not having a right fork available, lets check if it is available before continuing.
void get_forks(p) { sem_wait(&forks[left(p)]); /* you can't actually check the value of a semaphore like below * but i don't want to make this pseudocode too complex */ if (forks[right(p)] > 0) { sem_wait(&forks[right[p]]); } else { sem_signal(&forks[left(p)]); }}
However this also has an issue(s)
Livelocks
Suppose all of the nerds pick up their left forks. They all simultaneously see that the right fork is not available, and all together put down the left fork. This repeats.
This is not a deadlock because they’re aren’t stuck waiting for a resource, rather for the condition that will enable them to use the resource.
The livelock could potentially be resolved by adding a random()/exponential() backoff function after every failed attempt to get a right fork, but that’s not very practical for us.
its a poor use of resources, we want the most concurrent solution remember
// TODO: come back to check correctness of this// as far as ive seen the signal() is put in putfork instead
Working Solution: 1 (Footman)
If only N−1 nerds eat at the same time, deadlock is impossible because one of them will always get the right fork. We can reframe this similar to the bounded buffer problem now, and treat the table of the buffer
This also prevents starvation because the moment a nerd beside the 5th one finishes eating, the waiting 5th nerd must be allowed to eat
Working Solution: 2 (Djikstra’s)
Notice how our problems all occur because all 5 nerds are lefties. What if one of them was a rightie??? 4 would pick up their left fork, the rightie would fail to pick up their right fork, leaving the 4th one to be able to pick up their right fork too. No deadlock :D
both of the neighbours aren’t eating, which means he gets the forks and eats
one of the neighbouts is eating, which means he waits for them to finish so he gets the forks
Starving Tannebaums
It is possible to starve in this solution due a repeating pattern of exclusion. Suppose P2 and P4 are currently eating, when P2 finishes, P1 starts, and when P4 finishes, P3 starts.
Once P3 is done, P4 starts, and once P1 is done, P2 starts. If this repeats, P0 will starve
Cigarette Smoker’s Problem
4 threads are involved, one agent and 3 smokers
smoking a cigarrete consists of three ingredients
tobcacco
paper
matches
the agent has an infinite supply of all 3 ingredients
each smoker has an infinite supply of one of the 3 ingredients. one has paper, the other tobcacco, the other matches.
the agent repeatedly chooses two of the ingredients at random and presents them to the smokers. The smoker with the complementary ingredient can pick them both up and smoke his cigarrete
There are three versions of this problem
Trivial Version:
the agent just signals the smoker than can now run based on the ingredients
Interesting Version:
you are not allowed to modify the agent code
Impossible Version:
you are not alloewed to modify the agent code
you can’t use conditional statements or an array of semaphores
We must avoid deadlock where the other two smokers take one ingredient each and wait for a third one that never comes
and so on for the three smokers may lead to Deadlock, because it is possible the tobacco smoker manages to acquire the match, and the match smoker acquires the paper. They are both now waiting for a resource that’s never going to come and no one can make a cigarrete
To mitigate this and only signal the correct smoker, we create helper threads. We run 3 helper threads with the following addition variables/semaphores
bool isTobacco = false, isPaper = false, isMatch = false;sem_t t_smoker = 0; // signal tobacco smoker to wake upsem_t m_smoker = 0; // signal match smoker to wake upsem_t p_smoker = 0; // signal paper smoker to wake up
We can also generalise this problem and say that the agent does not wait for smokers, instead it keeps pushing ingredients on the table as it wants. Then you will need to keep count of the ingredients present instead of just boolean values.
Thread Throttling
To limit the max number of threads working on something together, you can use a counting semaphore.