Example: Too-Much-Milk
| Time | You | Your Roommate |
| 3:00 | Arrive Home | |
| 3:05 | Look in fridge; no milk | |
| 3:10 | Leave for grocery | |
| 3:15 | Arrive home | |
| 3:20 | Arrive at grocery | Look in fridge; no milk |
| 3:25 | Buy milk | Leave for grocery |
| 3:35 | Arrive home; put milk in fridge | |
| 3:45 | Buy milk | |
| 3:50 | Arrive home; oh no! |
Synchronization: Using atomic actions to ensure cooperation between threads.
Mutual Exclusion: Ensuring that only one thread does a particular activity at a time. One thread doing that activity excludes all the others from doing the same thing at that time.
Critical Section: A piece of code that can be executed by only one thread at a time.
Lock: Prevents other threads from doing something. The thread that performs the Lock operation is said to acquire the lock and to be the owner of it.
Thread Aif (noMilk & noNote) leave Note; buy milk; remove Note; |
Thread Bif (noMilk & noNote) leave Note; buy milk; remove Note; |
Does this work? No, you may wind up with two milks.
Solution 2
Thread A:leave noteA; if (no noteB) if (noMilk) buy milk; remove NoteA; |
Thread B:leave noteB; if (no noteA) if (noMilk) buy milk; remove NoteB; |
Does this work? No, you may wind up with no milk.
Solution 3
Thread A:leave noteA; while (noteB) do nothing; if (noMilk) buy milk; remove NoteA; |
Thread B:leave noteB; if (no noteA) if (noMilk) buy milk; remove NoteB; |
Does this work? Yes.
Is Solution 3 a good solution? No.
Thread A:Lock->Acquire(); if (noMilk) buy milk; Lock->Release(); |
Thread B:Lock->Acquire(); if (noMilk) buy milk; Lock->Release(); |
Two questions:
Most uniprocessor architectures supply an atomic read-modify-write instruction that is easily implemented in hardware. Some examples:
int Test&Set(int value) { // all done atomically!
int temp;
temp = value;
value = 1;
return temp;
}
class Lock {
public:
Lock();
void Acquire();
void Release();
private:
int value;
}
Lock::Lock() {
value = 0;
}
Lock::Acquire() {
while (Test&Set(value) == 1) ; // busy wait until the lock is free
}
Lock::Release() {
value = 0;
}
What's wrong with this solution?
With Test&Set, we can't get rid of the busy waiting, but we can minimize it by busy waiting only to check the lock value. If the lock is busy, the the thread gives up the CPU. To do so, we need to provide routines to put threads to sleep and wake them up:
Thread::Sleep() {
remove thread from ready queue;
add thread to waiting queue;
}
Wakeup(Thread T) {
remove T from waiting queue;
add T to ready queue;
}
Now we can implement Locks efficiently:
class Lock {
public:
Lock();
void Acquire();
void Release();
private:
int value;
int guard;
}
Lock::Lock() {
value = FREE;
guard = 0;
}
Lock::Acquire(Thread T) {
while (Test&Set(guard) == 1) ; // busy wait
if (value != FREE) {
put T on queue Q;
T->Sleep();
guard = 0; // The O/S must do this after T has gone to sleep
}
else {
value = BUSY;
guard = 0;
}
}
Lock::Release() {
while (Test&Set(guard) == 1) ; // busy wait
if Q is not empty {
choose a thread T from Q;
Wakeup(T);
}
else
value = FREE;
}
We can get rid of busy waiting entirely by having the O/S implement locks in software:
class Lock {
public:
Lock();
void Acquire(T: Thread);
void Release();
private:
int value;
Queue Q;
}
Lock::Lock() {
// lock is free
value = 0;
// queue is empty
Q = nil;
}
|
Lock::Acquire(T: Thread) {
disable interrupts;
if (value == 1) {
add T to queue Q;
T->Sleep();
}
else {
value = 1;
enable interrupts;
}
}
Lock::Release() {
disable interrupts;
if (Q is not empty) {
remove a thread T from Q;
Wakeup(T);
}
else {
value = 0;
}
enable interrupts;
}
|
Question: When does Acquire enable interrupts?
Would it be safe to enable interrupts before adding the thread T to the queue? No, Release might check the queue before T is added to the queue. T would not get waked up.
Would it be safe to enable interrupts after putting the thread on the queue but before the thread goes to sleep? No, Release might put T back on the ready queue before T goes to sleep. In that case, when T wakes up, it will go right back to sleep, effectively missing the wakeup call.
But if the thread is already asleep, then how can it enable interrupts? It can't; some other thread must do that for it. In Nachos, Thread::Sleep() disables interrupts, but it is the responsibility of the next thread that executes to enable interrupts.
A condition variable is a queue of threads waiting inside a critical section. Condition variables support three operations:
class Queue {
public:
Queue();
void AddToQueue(ItemType);
ItemType RemoveFromQueue();
private:
Lock lock;
Condition notEmpty;
Condition notFull;
... // queue data structures
}
Queue::Queue() {
initialize an empty queue;
}
void Queue::AddToQueue(ItemType item) {
lock->Acquire();
while (queue is full)
notFull->Wait(lock);
put item in queue;
notEmpty->Signal();
lock->Release();
}
ItemType Queue::RemoveFromQueue() {
lock->Acquire();
while (queue is empty)
notEmpty->Wait(lock);
remove item from queue;
notFull->Signal();
lock->Release();
return item;
}
Semantics of Condition Variables
What happens when a thread issues a signal on a condition variable? There are two popular answers:
Hoare semantics guarantee that whatever condition T was waiting for will still hold true when T resumes processing, assuming that the condition involves data structures protected by the lock and that it is true when S signals. Since T immediately gets the lock, no other thread, including S, can access the data structures before T does:
if (conditions the thread needs are not true) condition->Wait();
Because T may not be the next thread to access the protected data structure, Mesa semantics can not guarantee that whatever condition T was waiting for will still hold true when T regains the lock. Hence when T reacquires the lock, it must test again and wait on the condition again if necessary:
while (conditions the thread needs are not true) condition->wait();
Monitors
Condition variables first arose in the context of a monitor, a high-level synchronization construct defined by Hoare in 1974. A monitor is similar to a C++ class that ties the data, operations and, in particular, the synchronizations all together. Here is a monitor version of our queue example, using Hoare semantics, with Pascal-like syntax to emphasize how monitors are different from classes defined in terms of locks and condition variables:
monitor queue;
not_empty, not_full: Condition;
... // queue data structures
procedure add(item_type item);
begin
if (queue is full) then wait(not_full);
put item in queue;
signal(not_empty)
end;
function remove: item_type;
item: item_type;
begin
if (queue is empty) then wait(not_empty);
remove item from queue;
remove := item
signal(not_full);
end;
begin
initialize empty queue data structures
end;
Note that, in Pascal style, this defines a single monitor, not a class of monitors.
Where are the locks? They are part of the underlying implementation. A thread wanting to access the queue must call one of the queue procedures (this is called entering the monitor). Only one thread at a time is allowed to be actively executing a monitor procedure; all other threads that are in the monitor must be sleeping. The underlying implementation takes care of ensuring this.