Home
6Synchronization Tools
6

Chapter 6 · Final syllabus · Lectures 18–20

Synchronization Tools

Race conditions, the critical-section problem and its three requirements, interrupt-based and software solutions, Peterson’s solution, test_and_set and compare_and_swap, atomic variables, mutex locks, semaphores and monitors with condition variables.

← → move between slides · F full screen · M slide list

Before we start

Why this matters

Two processes add one to the same counter. Sometimes the counter goes up by one, not two.

When cooperating processes share data, the order of their instructions matters — and preemption can interleave them at the worst moment.

This chapter defines what a correct solution must guarantee and builds the tools for it, from Peterson’s algorithm to semaphores and monitors.

Chapter 6 · Lectures 18–20

Outline

Select a topic to jump to the slide that explains it.

  1. Background
  2. The critical-section problem
  3. Peterson’s solution
  4. Hardware support for synchronization
  5. Mutex locks
  6. Semaphores
  7. Monitors

Background

Background

  • Processes can execute concurrently
    • May be interrupted at any time, partially completing execution
  • Concurrent access to shared data may result in data inconsistency
  • Maintaining data consistency requires mechanisms to ensure the orderly execution of cooperating processes

Background

Race condition

  • Processes P0 and P1 are creating child processes using the fork() system call
  • Race condition on kernel variable next_available_pid which represents the next available process identifier (pid)
  • Unless there is a mechanism to prevent P0 and P1 from accessing the variable next_available_pid the same pid could be assigned to two different processes!
A race condition is when the outcome depends on the particular order in which the accesses take place. Try it on the next slide.

Interactive

Race on next_available_pid

Code and output

A real race condition

race.c — two threads, one counter
#include <pthread.h>
#include <stdio.h>

#define N 1000000
long counter = 0;                       /* shared data */
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int use_lock;

void *work(void *arg)
{
    for (int i = 0; i < N; i++) {
        if (use_lock) pthread_mutex_lock(&lock);    /* entry section */
        counter++;                                  /* critical section */

Code and output

A real race condition (cont.)

        if (use_lock) pthread_mutex_unlock(&lock);  /* exit section */
    }
    return NULL;
}

int main(void)
{
    for (use_lock = 0; use_lock <= 1; use_lock++) {
        pthread_t a, b;
        counter = 0;
        pthread_create(&a, NULL, work, NULL);
        pthread_create(&b, NULL, work, NULL);
        pthread_join(a, NULL);
        pthread_join(b, NULL);

Code and output

A real race condition (cont.)

        printf("%-12s counter = %ld (expected %d)\n",
               use_lock ? "with lock:" : "no lock:", counter, 2 * N);
    }
    return 0;
}
Output
Terminal
$ gcc race.c -o race -pthread && ./race
no lock:     counter = 1032343 (expected 2000000)
with lock:   counter = 2000000 (expected 2000000)

counter++ is three machine steps (load, add, store). Without the lock the threads overwrite each other’s updates, so the total is wrong and different on every run. With the mutex around the critical section it is always 2,000,000.

The critical-section problem

Critical section problem

  • Consider system of n processes {p0, p1, … pn−1}
  • Each process has critical section segment of code
    • Process may be changing common variables, updating table, writing file, etc.
    • When one process in critical section, no other may be in its critical section
  • Critical section problem is to design protocol to solve this
  • Each process must ask permission to enter critical section in entry section, may follow critical section with exit section, then remainder section

General structure of process Pi:

while (true) {

    entry section

        critical section

    exit section

        remainder section
}

The critical-section problem

Critical-section problem (cont.)

Requirements for solution to critical-section problem:

  1. Mutual exclusion – if process Pi is executing in its critical section, then no other processes can be executing in their critical sections
  2. Progress – if no process is executing in its critical section and there exist some processes that wish to enter their critical section, then the selection of the process that will enter the critical section next cannot be postponed indefinitely
  3. Bounded waiting – a bound must exist on the number of times that other processes are allowed to enter their critical sections after a process has made a request to enter its critical section and before that request is granted
    • Assume that each process executes at a nonzero speed
    • No assumption concerning relative speed of the n processes

The critical-section problem

Interrupt-based solution

  • Entry section: disable interrupts
  • Exit section: enable interrupts
  • Will this solve the problem?
    • What if the critical section is code that runs for an hour?
    • Can some processes starve – never enter their critical section.
    • What if there are two CPUs?
Disabling interrupts only stops preemption on one CPU. On a multiprocessor, a process on another CPU can still enter the critical section, and turning interrupts off on every CPU is too slow.

Peterson’s solution

Software solution

  • Two process solution
  • Assume that the load and store machine-language instructions are atomic; that is, cannot be interrupted
  • The two processes share one variable:
    • int turn;
  • The variable turn indicates whose turn it is to enter the critical section
  • initially, the value of turn is set to i

Algorithm for process Pi:

while (true) {
    while (turn == j);

Peterson’s solution

Software solution (cont.)

    /* critical section */

    turn = j;

    /* remainder section */
}

Correctness of the software solution

  • Mutual exclusion is preserved: Pi enters critical section only if turn = i, and turn cannot be both 0 and 1 at the same time
  • But progress fails: the processes must strictly alternate. If Pj stays in its remainder section, Pi cannot enter twice in a row.

Peterson’s solution

Peterson’s solution

  • Two process solution
  • Assume that the load and store machine-language instructions are atomic; that is, cannot be interrupted
  • The two processes share two variables:
    • int turn;
    • boolean flag[2]
  • The variable turn indicates whose turn it is to enter the critical section
  • The flag array is used to indicate if a process is ready to enter the critical section.
    • flag[i] = true implies that process Pi is ready!

Algorithm for process Pi:

while (true) {
    flag[i] = true;
    turn = j;
    while (flag[j] && turn == j)
        ;

    /* critical section */

    flag[i] = false;

    /* remainder section */
}

Peterson’s solution

Correctness of Peterson’s solution

Provable that the three CS requirements are met:

  1. Mutual exclusion is preserved: Pi enters CS only if either flag[j] = false or turn = i
  2. Progress requirement is satisfied
  3. Bounded-waiting requirement is met
If both processes want to enter, both set their flag; turn can only hold one value, the one written last, so exactly one of them waits. A process that is not interested has its flag false and never blocks the other.

Interactive

Step through both algorithms

Hardware support for synchronization

Synchronization hardware

  • Many systems provide hardware support for implementing the critical section code.
  • Uniprocessors – could disable interrupts
    • Currently running code would execute without preemption
    • Generally too inefficient on multiprocessor systems
      • Operating systems using this not broadly scalable
  • We will look at two forms of hardware support:
    • 1. Hardware instructions
    • 2. Atomic variables

Hardware support for synchronization

Hardware instructions

Special hardware instructions that allow us to either test-and-modify the content of a word, or to swap the contents of two words atomically (uninterruptedly):

The test_and_set instruction

boolean test_and_set (boolean *target)
{
    boolean rv = *target;
    *target = true;
    return rv;
}
  • Executed atomically
  • Returns the original value of passed parameter
  • Set the new value of passed parameter to true

Hardware support for synchronization

Hardware instructions (cont.)

The compare_and_swap instruction

int compare_and_swap(int *value, int expected, int new_value)
{
    int temp = *value;
    if (*value == expected)
        *value = new_value;
    return temp;
}
  • Executed atomically
  • Returns the original value of passed parameter value
  • Set the variable value the value of the passed parameter new_value but only if *value == expected is true. That is, the swap takes place only under this condition.

Hardware support for synchronization

Atomic variables

  • Typically, instructions such as compare-and-swap are used as building blocks for other synchronization tools.
  • One tool is an atomic variable that provides atomic (uninterruptible) updates on basic data types such as integers and booleans.
  • For example:
    • Let sequence be an atomic variable
    • Let increment() be operation on the atomic variable sequence
    • The command increment(&sequence); ensures sequence is incremented without interruption

Hardware support for synchronization

Atomic variables (cont.)

The increment() function can be implemented as follows:

void increment(atomic_int *v)
{
    int temp;

    do {
        temp = *v;
    }
    while (temp != compare_and_swap(v, temp, temp+1));
}
If another thread changed *v between the read and the swap, compare_and_swap fails (returns a different value) and the loop simply tries again.

Mutex locks

Mutex locks

  • Previous solutions are complicated and generally inaccessible to application programmers
  • OS designers build software tools to solve critical section problem
  • Simplest is mutex lock
    • Boolean variable indicating if lock is available or not
  • Protect a critical section by
    • First acquire() a lock
    • Then release() the lock
  • Calls to acquire() and release() must be atomic
    • Usually implemented via hardware atomic instructions such as compare-and-swap.

Mutex locks

Mutex locks (cont.)

  • But this solution requires busy waiting
    • This lock therefore called a spinlock

Solution to CS problem using mutex locks

while (true) {
    acquire lock

        critical section

    release lock

        remainder section
}

Semaphores

Semaphore

  • Synchronization tool that provides more sophisticated ways (than mutex locks) for processes to synchronize their activities.
  • Semaphore S – integer variable
  • Can only be accessed via two indivisible (atomic) operations
    • wait() and signal()
    • Originally called P() and V()

Definition of the wait() operation:

wait(S) {
    while (S <= 0)
        ; // busy wait
    S--;
}

Definition of the signal() operation:

signal(S) {
    S++;
}

Semaphores

Semaphore (cont.)

  • Counting semaphore – integer value can range over an unrestricted domain
  • Binary semaphore – integer value can range only between 0 and 1
    • Same as a mutex lock
  • Can implement a counting semaphore S as a binary semaphore
  • With semaphores we can solve various synchronization problems

Interactive

Bounded buffer with semaphores

Code and output

Bounded buffer in C

boundedbuf.c
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>

#define BUFFER_SIZE 3
int buffer[BUFFER_SIZE], in = 0, out = 0;
sem_t empty, full;                 /* counting semaphores */
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *producer(void *arg)
{
    for (int item = 1; item <= 5; item++) {
        sem_wait(&empty);              /* wait for a free slot   */
        pthread_mutex_lock(&mutex);
        buffer[in] = item; in = (in + 1) % BUFFER_SIZE;
        printf("produced %d\n", item);
        pthread_mutex_unlock(&mutex);

Code and output

Bounded buffer in C (cont.)

        sem_post(&full);               /* one more full slot     */
    }
    return NULL;
}

void *consumer(void *arg)
{
    for (int k = 0; k < 5; k++) {
        sem_wait(&full);               /* wait for an item       */
        pthread_mutex_lock(&mutex);
        int item = buffer[out]; out = (out + 1) % BUFFER_SIZE;
        printf("        consumed %d\n", item);
        pthread_mutex_unlock(&mutex);
        sem_post(&empty);              /* one more free slot     */
    }
    return NULL;
}

int main(void)

Code and output

Bounded buffer in C (cont.)

{
    pthread_t p, c;
    sem_init(&empty, 0, BUFFER_SIZE);  /* all slots free */
    sem_init(&full, 0, 0);             /* no items yet   */
    pthread_create(&p, NULL, producer, NULL);
    pthread_create(&c, NULL, consumer, NULL);
    pthread_join(p, NULL);
    pthread_join(c, NULL);
    return 0;
}
Output
Terminal · sample run on Linux
$ gcc boundedbuf.c -o boundedbuf -pthread && ./boundedbuf
produced 1
produced 2
produced 3
        consumed 1
        consumed 2
        consumed 3
produced 4
produced 5
        consumed 4
        consumed 5

One possible order: the producer fills all 3 slots, then must wait on empty until the consumer frees one. The order changes from run to run, but the buffer never overflows.

Semaphores

Semaphore implementation

  • Must guarantee that no two processes can execute the wait() and signal() on the same semaphore at the same time
  • Thus, the implementation becomes the critical section problem where the wait and signal code are placed in the critical section
  • Could now have busy waiting in critical section implementation
    • But implementation code is short
    • Little busy waiting if critical section rarely occupied
  • Note that applications may spend lots of time in critical sections and therefore this is not a good solution

Semaphores

Semaphore implementation (cont.)

Semaphore implementation with no busy waiting

  • With each semaphore there is an associated waiting queue
  • Each entry in a waiting queue has two data items:
    • Value (of type integer)
    • Pointer to next record in the list
  • Two operations:
    • block – place the process invoking the operation on the appropriate waiting queue
    • wakeup – remove one of processes in the waiting queue and place it in the ready queue
typedef struct {
    int value;
    struct process *list;
} semaphore;

Monitors

Monitors

  • A high-level abstraction that provides a convenient and effective mechanism for process synchronization
  • Abstract data type, internal variables only accessible by code within the procedure
  • Only one process may be active within the monitor at a time
  • Pseudocode syntax of a monitor:
monitor monitor-name
{
    // shared variable declarations
    procedure P1 (...) { .... }

    procedure P2 (...) { .... }

    procedure Pn (...) { ...... }

    initialization code (...) { ... }
}

Monitors

Monitors (cont.)

Schematic view of a monitor.

Monitors

Monitor implementation using semaphores

  • Variables
    • semaphore mutex
    • mutex = 1
  • Each procedure P is replaced by
wait(mutex);
    ...
    body of P;
    ...
signal(mutex);
  • Mutual exclusion within a monitor is ensured

Condition variables

condition x, y;
  • Two operations are allowed on a condition variable:
    • x.wait() – a process that invokes the operation is suspended until x.signal()
    • x.signal() – resumes one of processes (if any) that invoked x.wait()
      • If no x.wait() on the variable, then it has no effect on the variable

Monitors

Monitor with condition variables

Monitor with condition variables.

Monitors

Resuming processes within a monitor

  • If several processes queued on condition variable x, and x.signal() is executed, which process should be resumed?
  • FCFS frequently not adequate
  • Use the conditional-wait construct of the form x.wait(c)
  • where:
    • c is an integer (called the priority number)
    • The process with lowest number (highest priority) is scheduled next

Worked example 1 of 3

Worked example 1

Show an interleaving in which P0 and P1 both get pid 2615 from next_available_pid = 2615.
Show the solution
  1. P0 reads next_available_pid (2615) into its register.
  2. P0 is preempted; P1 reads next_available_pid (still 2615).
  3. P1 writes 2616 and gives its child pid 2615.
  4. P0 resumes, writes 2616, and also gives its child pid 2615.

Both children get 2615 and the counter only went up by one. The read-update-use sequence must be a critical section.

Worked example 2 of 3

Worked example 2

In Peterson’s solution, both processes set their flag at the same time; P0 then writes turn = 1 and P1 writes turn = 0. Who enters?
Show the solution

turn ends as 0 (P1 wrote last). P0 tests flag[1] && turn == 1: turn is 0, so the test is false and P0 enters. P1 tests flag[0] && turn == 0: true, so P1 waits until P0 sets flag[0] = false.

The process that writes turn last lets the other go first.

Worked example 3 of 3

Worked example 3

A counting semaphore S = 2 guards two printers. P1, P2 and P3 call wait(S) in order, then P1 calls signal(S). Trace S with the waiting-queue implementation.
Show the solution

P1: S = 1, enters. P2: S = 0, enters. P3: S = −1 < 0, blocked on the queue. P1 signal: S = 0; since S ≤ 0, wake up P3, which now uses the printer.

A negative value −k means k processes are waiting.

Practice

Practice (1 of 2)

Try each one on paper first, then check your answer.

1. State the three requirements of a solution to the critical-section problem.
Show the answer
Mutual exclusion, progress and bounded waiting.
2. Why is disabling interrupts not a good solution on multiprocessors?
Show the answer
It only prevents preemption on the CPU where it is done; processes on other CPUs can still enter. Disabling on all CPUs is slow, and long critical sections would block everything and starve others.
3. Which requirement does the shared-turn-only algorithm violate? Explain.
Show the answer
Progress: the processes must strictly alternate. If Pj stays in its remainder section with turn = j, Pi cannot enter even though nobody is in the critical section.

Practice

Practice (2 of 2)

Try each one on paper first, then check your answer.

4. What does test_and_set return and what does it store?
Show the answer
It atomically returns the original value of *target and sets *target to true. A lock is acquired when it returns false.
5. What is the difference between a binary semaphore and a counting semaphore?
Show the answer
A binary semaphore ranges only between 0 and 1 (like a mutex lock); a counting semaphore can range over an unrestricted domain, e.g. the number of identical resources.
6. Why does a monitor guarantee mutual exclusion?
Show the answer
Only one process may be active within the monitor at a time; implemented with a semaphore mutex = 1, each procedure is wrapped in wait(mutex) … signal(mutex).

Check yourself

Quiz

Summary · End of Chapter 6

Summary

  • Concurrent access to shared data can leave it inconsistent; a race condition is when the result depends on the order of access.
  • A critical-section solution needs mutual exclusion, progress and bounded waiting.
  • Disabling interrupts fails on multiprocessors; the shared-turn algorithm breaks progress; Peterson’s solution (turn + flag[2]) meets all three.
  • Hardware offers atomic test_and_set and compare_and_swap, used to build atomic variables and mutex locks (spinlocks).
  • Semaphores (counting or binary) use atomic wait() and signal(); a waiting queue with block and wakeup avoids busy waiting.
  • Monitors allow one active process at a time; condition variables provide x.wait() and x.signal(), with x.wait(c) for priority.