Chapter 5
Concurrency: Mutual Exclusion and Synchronization


The Two-Much-Milk Problem

What kind of knowledge and mechanisms do we need to get independent processes to communicate and get a consistent view of the world (computer state)?

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 Terminology

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.


Solving the Too-Much-Milk Problem

Correctness Properties: We want to ensure the following.

We will restrict ourselves to atomic loads and stores as building blocks:

Solution 1

Thread A
if (noMilk & noNote)
leave Note;
buy milk;
remove Note;
Thread B
if (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.

How do we get a simple, symmetrical solution that does not involve busy waiting?


A Solution Using Locks

One way is by using locks. There are two atomic operations on locks:

Our solution requires only that:

Solution 4

Thread A:
Lock->Acquire();
if (noMilk) buy milk;
Lock->Release();
Thread B:
Lock->Acquire();
if (noMilk) buy milk;
Lock->Release();

Two questions:


Implementing Locks

How do we make Acquire and Release atomic? There are two ways that a thread can lose control of the CPU:

Implementing locks in hardware

Most uniprocessor architectures supply an atomic read-modify-write instruction that is easily implemented in hardware. Some examples:

Example: Implementing locks with Test&Set

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?

A better implementation

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;
}

Software Implementations of Locks

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.


Condition Variables

Now that we have locks and can implement critical sections safely, one problem still remains: Sometimes a thread needs to go to sleep while inside its critical section (while waiting for something to be put into a buffer, for example), but if it holds onto the lock while it sleeps, then other threads will not be able to wake it up. Condition variables solve this problem by atomically releasing the lock when the thread is put to sleep.

A condition variable is a queue of threads waiting inside a critical section. Condition variables support three operations:

Rules for using condition variables:

  1. Each condition variable is associated with a lock.

  2. Several condition variables may be associated with the lock.

  3. A thread must hold the lock when doing operations on a condition variable.

Example: A Bounded Queue

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: