Using semaphores, in my opinion, improves the narrative.
- Strip out exposed int, mutex, and condition_variable_any and replace with single semaphore.
- No longer need separate waitForPermission and grantPermission functions. #like
- One last time, with feeling (code is right here):
static mutex forks[kNumForks];
static semaphore numAllowed(kNumForks - 1);
static void eat(unsigned int id) {
unsigned int left = id;
unsigned int right = (id + 1) % kNumForks;
numAllowed.wait(); // atomic -- that blocks on attempt to decrement 0
forks[left].lock();
forks[right].lock();
cout << oslock << id << " starts eating om nom nom nom." << endl << osunlock;
sleep_for(getEatDuration());
cout << oslock << id << " all done eating." << endl << osunlock;
numAllowed.signal(); // atomic ++, never blocks, possibly unblocks other waiting threads
forks[left].unlock();
forks[right].unlock();
}
- Parting comments:
- It's easy to understand the transactional ++ and -- that comes with signal and wait.
- The thread yield that comes with a wait on a semaphore value of 0 is more difficult to understand. Given that a semaphore represents a shared, limited resource, blocking and doing nothing until that resource becomes available is almost always the right thing to do.
- Make sure you understand the many pros on this approach over the busy waiting approach we initially used to avert the threat of deadlock.
- Can you think of any situations when busy waiting (also called spin locking) might be the right approach? #thoughtquestion