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.
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_pidwhich represents the next available process identifier (pid) - Unless there is a mechanism to prevent P0 and P1 from accessing the variable
next_available_pidthe same pid could be assigned to two different processes!
Interactive
Race on next_available_pid
Code and output
A real race condition
#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;
}$ 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:
- Mutual exclusion – if process Pi is executing in its critical section, then no other processes can be executing in their critical sections
- 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
- 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?
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
turnindicates whose turn it is to enter the critical section - initially, the value of
turnis 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, andturncannot 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
turnindicates whose turn it is to enter the critical section - The
flagarray is used to indicate if a process is ready to enter the critical section.flag[i] = trueimplies 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:
- Mutual exclusion is preserved: Pi enters CS only if either
flag[j] = falseorturn = i - Progress requirement is satisfied
- Bounded-waiting requirement is met
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
valuethe value of the passed parameternew_valuebut only if*value == expectedis 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
sequencebe an atomic variable - Let
increment()be operation on the atomic variablesequence - The command
increment(&sequence);ensuressequenceis incremented without interruption
- Let
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));
}*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
- First
- Calls to
acquire()andrelease()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()andsignal()- Originally called
P()andV()
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
#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;
}$ 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 5One 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()andsignal()on the same semaphore at the same time - Thus, the implementation becomes the critical section problem where the
waitandsignalcode 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.)
Monitors
Monitor implementation using semaphores
- Variables
semaphore mutexmutex = 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 untilx.signal()x.signal()– resumes one of processes (if any) that invokedx.wait()- If no
x.wait()on the variable, then it has no effect on the variable
- If no
Monitors
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 the solution
- P0 reads next_available_pid (2615) into its register.
- P0 is preempted; P1 reads next_available_pid (still 2615).
- P1 writes 2616 and gives its child pid 2615.
- 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
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
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.
Show the answer
Show the answer
Show the answer
Practice
Practice (2 of 2)
Try each one on paper first, then check your answer.
Show the answer
Show the answer
Show the answer
Check yourself
Quiz
Resources
Further learning
Free material to go deeper. Links open in a new tab.
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.