Concurrency is the idea that multiple tasks can be executed at onceโinterleaving executing ๐งถ Threads to give that impression that a program to doing thing. To avoid unexpected behavior, we need ๐ฅ Synchronization, usually with ๐ Lockes or ๐ Semaphores.
Across different systems, there are some common concurrency design patterns.
Producer-Consumer
We have producer and consumers threads; the former creates data and adds it to a shared data structure (such as a queue), and the latter removes one item from the shared structure and processes it.
The key problem here is that we need to avoid two threads from manipulating the data structure at the same time. Both producers and consumers need to lock the shared data structure when using it.
Mutex with Condition Variable
There will be situations where the shared data structure is empty, and the consumer has nothing to do but to keep checking it. To avoid busy loops, we can use โ๏ธ Condition Variables to wait until the data structure is updated. Some pseudo-code for the producer and consumer are below:
void consumer() {
while (1) {
acquire_lock();
while (q.empty()) {
cond_wait();
}
data = q.pop();
release_lock();
}
}
void producer() {
while (1) {
acquire_lock();
q.push(data);
cond_signal();
release_lock();
}
}
Semaphores
If the queue has a finite capacity of
emptyS
initialized totracks the number of empty slots. fullS
initialized totracks the number of occupied slots. mutex
controls access to the queue.
void consumer() {
while (1) {
fullS.V(); // Block if queue is empty
mutex.P();
data = queue.pop();
mutex.V();
emptyS.V();
}
}
void producer() {
while (1) {
emptyS.P(); // Block if queue is full
mutex.P();
queue.push(data);
mutex.V();
fullS.V();
}
}
Reader-Writer
We have reader and writer threads that access shared data. While multiple readers can safety read the data at the same time, we can only have one writer at a time; during a write, nothing else can access the data.
Mutex with Condition Variable
We have one lock, but weโll have two ways to acquire itโone for reader, one for writer.
- For reader, weโll acquire the lock, then wait until thereโs no writer active and thereโs no writer waiting. Afterwards, weโre safe to read.
- For writer, weโll acquire the lock, then wait while thereโs another active writer or any active readers. Afterwards, weโre safe to write.
When releasing the lock, both readers and writers broadcast a signal to wake up other sleeping threads.
There are some pitfalls regarding liveness to watch out for:
- If readers always wait until there are no writers, readers can starve if we keep having new writers. Similarly, if writers wait until there are no readers, writers can starve if we keep having new readers.
- We canโt guarantee that a specific thread will run after a signal. If we have multiple threads waiting, itโs possible that one thread will just be unlucky and never run.
Mutex without Condition Variable
We can use a mutex rw
thatโs needed before accessing the shared data. To allow multiple readers, weโll let only the first reader lock the mutex. We can track the number of active readers in an integer and use another mutex mutex
to protect the counter.
void writer() {
while (1) {
rw.P();
buffer.write();
rw.V();
}
}
void reader() {
while (1) {
mutex.P();
readcount++;
if (readcount == 1)
rw.P();
mutex.V();
buffer.read();
mutex.P();
readcount--;
if (readcount == 0)
rw.V();
mutex.V();
}
}
Dining Philosophers
We have multiple philosophers sitting around a table, and between every two philosophers is one chopstick. For a philosopher to eat, they must acquire the chopsticks on both sides; however, the philosophers on their sides are also trying to acquire the chopstick between them.
In this setup, we want to allow all philosophers to take turns eatingโwithout encountering ๐ Deadlocks or starvation.
- In the naive solution where each philosopher tries to acquire the left chopstick, then the right, itโs possible for all of them to lock their left chopstick, resulting in a deadlock.
- We can instead have a global lock on the โright to pick up chopsticks.โ Philosophers take turns trying to acquire both chopsticks and only hold them if both succeed. This prevents deadlocks, but itโs still susceptible to starvation.
- Alternatively, philosophers can just try to get a chopstick, and if they canโt eat, they release the lock and wait a bit before trying again. This again avoids deadlock but doesnโt guard starvation.
- A simpler method is for philosophers to alternate their pick-up order: even-numbered ones grab the left chopstick first, and odd-numbered ones grab the right first. This avoids deadlock, but starvation is still possible.
- Have a lock manager, forcing the philosophers to ask the manager for a lock. This impacts scalability.
- Enforce a hierarchy by numbering the chopsticks, then require each philosopher to pick up the chopstick with lower number first. This is similar to the disrupted pick-up order approach above.
- Chandy/Misra solution: chopsticks start off dirty, and once philosopher eats, chopsticks are dirty. When a philosopher needs a chopstick they canโt get, ask neighbor: if itโs dirty, clean it and give it up; otherwise, keep it (and defer the request).
A simpler solution could be to add a timeout to lock requests, thereby allowing philosophers to โabort and retryโ after a while. While this prevents deadlocks, introduces another issue: livelocks, whether the philosophers can abort and retry in lockstep.
Sleeping Barber
We have a barbershop with one barber and multiple customers. When the barber is finished with a customer, they go to the waiting area and find another customer; if none are there, they go to their chair and sleep. If a customer arrives and the barber is sleeping, the customer wakes them up; otherwise, they just sit down and wait.
The key challenge here is to avoid the case where the customer waits, but the barber finishes before the customer sits down, thereby seeing no sitting customers and going to sleepโdeadlock!
Semaphores
With
mutex
protects a counter keeping track of the number of free seats.sittingCustomer
initialized toblocks the barber when thereโs nothing to do. barberReady
initialized toblocks the customers when the barber is busy.
void customer() {
mutex.P();
if (freeSeats > 0) {
freeSeats--;
sittingCustomer.V();
mutex.V();
barberReady.P();
// Have haircut here
} else {
mutex.V();
}
}
void barber() {
while (1) {
sittingCustomer.P();
mutex.P();
freeSeats++;
barberReady.V();
mutex.V();
// Cut hair here
}
}