Locks and Condition Variables
Optional readings for this topic from Operating Systems: Principles and Practice: Sections 5.2-5.4.
Needed: higher-level synchronization mechanism that provides
- Mutual exclusion: easy to create critical sections
- Blocking: delay a thread until some desired event occurs
Locks
Lock: an object that can only be owned by a single thread
at any given time (C++ class std::mutex
). Operations on a lock:
lock
: mark the lock as owned by the current thread; if some other thread already owns the lock then first wait until the lock is free. Lock typically includes a queue to keep track of waiting threads.unlock
: mark the lock as free (it must currently be owned by the calling thread).
Too much milk solution with locks (using C++ library APIs):
std::mutex mutex;
...
mutex.lock();
if (milk == 0) {
buy_milk();
}
mutex.unlock();
A more complex example: producer/consumer.
- Producers add characters to a buffer
- Consumers remove characters from the buffer
- Characters will be removed in the same order added
- Version 1:
class Pipe { Pipe() {} void put(char c); char get(); std::mutex mutex; char buffer[SIZE]; int count = 0; int nextPut = 0; int nextGet = 0; }; void Pipe::put(char c) { mutex.lock(); count++; buffer[nextPut] = c; nextPut++; if (nextPut == SIZE) { nextPut = 0; } mutex.unlock(); } char Pipe::get() { char c; mutex.lock(); count--; c = buffer[nextGet]; nextGet++; if (nextGet == SIZE) { nextGet = 0; } mutex.unlock(); return c; }
- Version 2: handle empty and full situations
class Pipe { Pipe() {} void put(char c); char get(); std::mutex mutex; char buffer[SIZE]; int count = 0; int nextPut = 0; int nextGet = 0; }; void Pipe::put(char c) { mutex.lock(); while (count == SIZE) { mutex.unlock(); mutex.lock(); } count++; buffer[nextPut] = c; nextPut++; if (nextPut == SIZE) { nextPut = 0; } mutex.unlock(); } char Pipe::get() { char c; mutex.lock(); while (count == 0) { mutex.unlock(); mutex.lock(); } count--; c = buffer[nextGet]; nextGet++; if (nextGet == SIZE) { nextGet = 0; } mutex.unlock(); return c; }
Condition Variables
Synchronization mechanisms need more than just mutual exclusion; also need a way to wait for another thread to do something (e.g., wait for a character to be added to the buffer)
Condition variables: used to wait for a particular state to be reached (e.g. characters in buffer).
- C++ class
std::condition_variable
. wait(lock)
: atomically release lock, put thread to sleep untilcondition
is notified; when thread wakes up again, re-acquire lock before returning.notify_one()
: if any threads are waiting oncondition
, wake up one of them.notify_all()
: same asnotify
, except wake up all waiting threads.- Note: after
notify
, notifying thread keeps lock, waking thread goes on the queue waiting for the lock. - Warning: when a thread wakes up after
wait
there is no guarantee that the desired condition still exists: another thread might have snuck in.
Producer/Consumer, version 3 (with condition variables):
class Pipe {
Pipe() {}
void put(char c);
char get();
std::mutex mutex;
std::condition_variable charAdded, charRemoved;
char buffer[SIZE];
int count = 0;
int nextPut = 0;
int nextGet = 0;
};
void Pipe::put(char c) {
mutex.lock();
while (count == SIZE) {
charRemoved.wait(mutex);
}
count++;
buffer[nextPut] = c;
nextPut++;
if (nextPut == SIZE) {
nextPut = 0;
}
charAdded.notify_one();
mutex.unlock();
}
char Pipe::get() {
char c;
mutex.lock();
while (count == 0) {
charAdded.wait(mutex);
}
count--;
c = buffer[nextGet];
nextGet++;
if (nextGet == SIZE) {
nextGet = 0;
}
charRemoved.notify_one();
mutex.unlock();
return c;
}
Monitors
How many locks should you use?
- More locks may permit more concurrency (less lock contention)
- But, more locks lead to complexity, potential for deadlock
- Lock acquisition is relatively expensive, so fewer locks may result in less overhead
- Best approach: as few locks as possible, while providing an acceptable level of lock contention.
Good general approach: associate a lock with a collection of related variables.
Monitor:
- A shared data structure
- A collection of procedures
- One lock that must be held whenever accessing the shared data (typically each procedure acquires the lock at the very beginning and releases the lock before returning).
- One or more condition variables used for waiting.
- The Pipe class is an example of a monitor.