Definition
Mutual Exclusion (Mutex) is a synchronization principle that ensures only one process/thread can access a shared resource at a time. It prevents race conditions by serializing access to critical sections. Think of it like a single bathroom that only one person can use at a time - others must wait outside.
Need for Mutual Exclusion
The Problem Without Mutual Exclusion
Shared Variable: balance = 1000
Thread 1: Thread 2:
read(1000) read(1000)
add 100 subtract 500
write(1100) write(500)
Expected: Could be 1100 or 500
Actual: Depends on timing - could be wrong value
The Solution: Mutual Exclusion
Thread 1:
LOCK
read(1000)
add 100
write(1100)
UNLOCK
Thread 2: (waits for lock)
LOCK (now acquired)
read(1100)
subtract 500
write(600)
UNLOCK
Requirements for Mutual Exclusion
Any solution must satisfy three conditions:
1. Safety (Mutual Exclusion)
At most one process in critical section at a time
Time: 0 1 2 3 4 5 6
Thread1:[CS]
Thread2: [waiting] [CS]
Thread3: [waiting] [waiting] [CS]
✓ Only one in CS at any time
2. Liveness (No Deadlock/Starvation)
If no process in critical section, waiting process eventually enters
If process wants CS and no one using it
→ It should enter eventually (not wait forever)
If multiple processes waiting
→ All should eventually get turn (no starvation)
3. Fairness (Bounded Waiting)
No process waits infinitely for critical section
If process requests CS, it enters within N requests from other processes
(N = reasonable bound, prevents starvation)
Mutual Exclusion Solutions
Solution 1: Software-Based Locks
Algorithm 1: Disable Interrupts
acquire_lock() {
disable interrupts; // Prevent context switching
}
release_lock() {
enable interrupts; // Allow context switching
}
Pros:
- Simple
- Guaranteed mutual exclusion
Cons:
- Only works on single CPU
- Disables interrupts = responsive devices can’t interrupt
- Unprivileged processes can’t call this
- System hangs if lock never released
Algorithm 2: Test and Set Lock
Hardware-provided atomic operation: test_and_set()
// Hardware atomic instruction
bool test_and_set(bool *lock) {
bool old = *lock;
*lock = true;
return old;
}
// Using it:
acquire_lock() {
while (test_and_set(&lock)) {
// Spin if lock taken
}
}
release_lock() {
lock = false;
}
Pros:
- Works on multi-CPU
- Hardware guarantees atomicity
Cons:
- Busy waiting (spinning) wastes CPU
- Unpredictable waiting time
- Starvation possible if one process keeps spinning
Algorithm 3: Dekker’s Algorithm
First software solution providing all three properties
Uses shared variables and software logic (no hardware support needed)
Complex but proven correct
Algorithm 4: Peterson’s Algorithm
Simpler than Dekker’s, also correct
int interested[2] = {0, 0};
int turn = 0;
// Process 0:
interested[0] = 1;
turn = 1;
while (interested[1] && turn == 1) {
// Spin (wait for Process 1 to finish)
}
// Critical Section
interested[0] = 0;
// Process 1: Similar with indices swapped
Properties:
- ✓ Mutual Exclusion
- ✓ Progress (no deadlock)
- ✓ Bounded Waiting (fairness)
Limitation: Only works for 2 processes
Solution 2: Hardware Support
Atomic Instructions
CPU provides instructions that execute atomically (can’t be interrupted):
-
Test-and-Set (TAS)
Atomic operation: Read value, set to 1, return old value -
Compare-and-Swap (CAS)
If memory == expected, set to new value Atomic and returns success/failure -
Load-Linked and Store-Conditional (LL/SC)
Read value (with watch) If watched value changed, store fails
Spin Lock
Using hardware atomic operation:
void acquire() {
while (test_and_set(&lock))
; // Spin
}
void release() {
lock = 0;
}
Problem: Spinning wastes CPU cycles!
Solution 3: OS-Level Primitives
Mutex Lock
lock.acquire(); // May sleep if unavailable
// Critical Section
lock.release(); // Wakes up waiting process
Much better than spinning!
Semaphore
Will be covered in detail in separate topic.
Spinlock vs Sleep Lock
Spinlock (Busy Waiting)
acquire() {
while (!available) {
// CPU spinning, doing nothing useful
}
}
Good for:
- Very short critical sections
- Multi-CPU systems (other CPU doing useful work)
Bad for:
- Long critical sections (wasted CPU)
- Single CPU (other process can’t run while spinning)
Sleep Lock (Blocking)
acquire() {
if (not available) {
// Process sleeps, CPU runs other processes
sleep();
}
}
Good for:
- Long critical sections
- Single CPU
- Many processes competing
Bad for:
- Very short critical sections (overhead of sleep/wake)
Practical Mutual Exclusion
In C/Unix (POSIX Threads)
#include <pthread.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&lock);
// Critical section
count = count + 1;
pthread_mutex_unlock(&lock);
In Java
// Method-level synchronization
synchronized void increment() {
count = count + 1;
}
// Block-level synchronization
synchronized (lock) {
count = count + 1;
}
In C#
lock (lockObject) {
count = count + 1;
}
Common Mistakes with Mutual Exclusion
1. Forgetting to Release Lock
acquire_lock();
critical_section();
// Forgot release_lock() - DEADLOCK!
2. Deadlock from Circular Wait
Thread 1: acquire(lockA), acquire(lockB)
Thread 2: acquire(lockB), acquire(lockA)
If Thread 1 has lockA and waits for lockB
And Thread 2 has lockB and waits for lockA
→ Deadlock!
3. Not Using Lock for All Access
if (count > 10) { // Not protected!
// Race condition here
}
lock.acquire();
count = count + 1;
lock.release();
4. Protecting Too Much (Low Concurrency)
lock.acquire();
slow_io_operation(); // Why hold lock during I/O?
count = count + 1;
lock.release();
Performance Implications
Mutex Overhead
- Time: Lock/unlock operations take time
- Complexity: Code becomes harder to understand
- Concurrency: Reduced parallelism (only one in CS)
Optimization Trade-offs
- Fine-grained locking: High concurrency but complex, deadlock risk
- Coarse-grained locking: Simple but low concurrency
Mutex in Multi-Core Systems
On Single Core
Mutual exclusion prevents one thread from interleaving with another.
On Multi-Core
Mutual exclusion prevents simultaneous execution on different cores.
Core 1: [CS] [CS] [CS]
Core 2: [waiting] [CS] [CS]
Mutual exclusion more critical here because threads truly run in parallel!
Summary
Mutual exclusion ensures only one process accesses shared resource, preventing race conditions. Solutions range from software (Peterson’s Algorithm) to hardware (atomic instructions) to OS-level (mutex locks). Modern systems use OS-level mutexes as they provide good performance and prevent busy-waiting. Proper use of mutual exclusion is essential for correct multi-threaded programs. Key challenge: Balancing correctness with performance and simplicity.