Lecture 12 - Locks
Instructor: Tej Chajed Topics: Locks, mutual exclusion, atomic operations, spinlocks, test-and-set, compare-and-swap, ticket locks
Learning Outcomes
- Compare lock implementations based on several goals
- Explain how hardware and OS support lock implementation
Why Locks?
Without locks, concurrent threads accessing shared data produce incorrect results due to race conditions. Locks ensure mutual exclusion — only one thread executes a critical section at a time.
Timeline View with Locks
Consider two threads both incrementing a shared variable *balance:
Thread 1 Thread 2
mutex_Lock() mutex_Lock()
mov 0x123, %eax
add $0x1, %eax
mov %eax, 0x123
mutex_unlock()
mov 0x123, %eax
add $0x2, %eax
mov %eax, 0x123
mutex_unlock()
How much is added to shared variable? 3: correct!
Lecture Note
With locks,
mutex_Lock()/*balance += 1/mutex_Unlock()andmutex_Lock()/*balance += 2/mutex_Unlock()run sequentially, producing the correct result.
Lock Implementation Goals
Correctness
- Mutual exclusion — only one thread in the critical section at a time
- Progress (deadlock-free) — if several simultaneous requests, must allow one to proceed
- Bounded (starvation-free) — must eventually allow each waiting thread to enter
Fairness
Each thread waits for the same amount of time.
Performance
CPU is not used unnecessarily.
Implementing Synchronization
Atomic operation: No other instructions can be interleaved.
Three approaches to implementing locks:
- Disable interrupts
- Locks using loads/stores
- Using special hardware instructions
Approach 1: Disable Interrupts
Turn off interrupts for critical sections:
- Prevents the dispatcher from running another thread
- Code between interrupts executes atomically
void acquire(lockT *l) {
disableInterrupts();
}
void release(lockT *l) {
enableInterrupts();
}
Disadvantages:
- Only works on uniprocessors (other CPUs are unaffected by disabling local interrupts)
- Process can keep control of CPU for arbitrary length
- Cannot perform other necessary work (e.g., handling I/O)
Approach 2: Locks with Load + Store
Use a single shared lock variable:
// shared variable
boolean lock = false;
void acquire(Boolean *lock) {
while (*lock) /* wait */ ;
*lock = true;
}
void release(Boolean *lock) {
*lock = false;
}
Lecture Note
falsemeans free,truemeans held. On release, you could checkif (*lock == false)and throw an error for “misuse of lock API.”
Race Condition with Load and Store
Does this work? No — testing the lock and setting the lock are not atomic.
*lock == 0 initially
Thread 1 Thread 2
while(*lock == 1)
while(*lock == 1)
*lock = 1
*lock = 1
Both threads grab the lock! The problem is that the check and the set are two separate operations that can be interleaved.
Atomic Hardware Instructions
To implement synchronization correctly, we need hardware support. Examples of atomic operations:
- Code between interrupts on uniprocessors (disable timer interrupts, don’t do any I/O)
- Special hardware instructions:
xchg(exchange) — also called Test&SetCompareAndSwap(CAS)FetchAndAdd(FAA)
Lecture Note
Loads and stores of single words are atomic individually, but combining them (test-then-set) is not atomic. The special hardware instructions solve this.
Test-and-Set (xchg)
The xchg instruction atomically swaps a value at an address and returns the old value:
// xchg(int *addr, int newval)
// return what was pointed to by addr
// at the same time, store newval into addr
int xchg(int *addr, int newval) {
int old = *addr;
*addr = newval;
return old;
}
In x86 assembly, this is a single instruction:
movl 4(%esp), %edx
movl 8(%esp), %eax
xchgl (%edx), %eax
ret
Spinlock with xchg
typedef struct __lock_t {
int flag;
} lock_t;
void init(lock_t *lock) {
lock->flag = 0;
}
void acquire(lock_t *lock) {
while (xchg(&lock->flag, 1) == 1)
; // spin-wait (do nothing)
}
void release(lock_t *lock) {
lock->flag = 0;
}
Lecture Note
flag = 0means free,flag = 1means held. Whenxchgreturns 0, we know we own the lock (we atomically set it to 1 and got back 0). Whenxchgreturns 1, the lock was already held — we learn we do not own it, but the value stays 1 (no harm done), so we retry.
Compare-and-Swap (CompareAndSwap)
int CompareAndSwap(int *addr, int expected, int new) {
int actual = *addr;
if (actual == expected) {
*addr = new;
return (1); // swap happens
}
return 0; // nothing happens
}
Lecture Note
If the value matches
expected, the swap happens and returns 1. Otherwise nothing changes and it returns 0.
Spinlock with CompareAndSwap
void acquire(lock_t *lock) {
while (CompareAndSwap(&lock->flag, 0, 1) == 0)
; // spin-wait (do nothing)
}
This tries to atomically change flag from 0 to 1. If flag is already 1 (held), CompareAndSwap returns 0 and the thread spins.
Basic Spinlocks are Unfair
With a basic spinlock (using xchg or CAS), thread A can repeatedly acquire the lock while thread B spins and waits:
Time: 0 20 40 60 80 100 120 140 160
[A] [B] [A] [B] [A] [B] [A] [B]
spin spin spin spin
- A unlocks and immediately re-locks before B can acquire
- B wastes its entire time slice spinning
- The scheduler is unaware of locks/unlocks!
Ticket Locks (Fair Spinlock)
Idea: Reserve each thread’s turn to use a lock. Each thread spins until their turn.
Uses a new atomic primitive, FetchAndAdd:
int FetchAndAdd(int *ptr) {
int old = *ptr;
*ptr = old + 1;
return old;
}
Lock state consists of two fields: ticket and turn.
- Acquire: Grab a ticket (via FAA); spin while thread’s ticket != turn
- Release: Advance to next turn
Lecture Note
FAA gives each thread its own ticket number. Example: A calls
lock()and gets ticket 0 (owner). B callslock()and gets ticket 1. C callslock()and gets ticket 2. When A callsunlock(), turn advances to 1, so B runs next. This guarantees FIFO ordering.
Ticket Lock Implementation
typedef struct __lock_t {
int ticket;
int turn;
} lock_t;
void lock_init(lock_t *lock) {
lock->ticket = 0;
lock->turn = 0;
}
void acquire(lock_t *lock) {
int myturn = FetchAndAdd(&lock->ticket);
// spin
while (lock->turn != myturn);
}
void release(lock_t *lock) {
FetchAndAdd(&lock->turn);
}
Ticket Lock Example
A lock(): gets ticket 0, spins until turn = 0 -> runs
B lock(): gets ticket 1, spins until turn = 1
C lock(): gets ticket 2, spins until turn = 2
A unlock(): turn++ (turn = 1)
B runs
A lock(): gets ticket 3, spins until turn = 3
B unlock(): turn++ (turn = 2)
C runs
C unlock(): turn++ (turn = 3)
A runs
A unlock(): turn++ (turn = 4)
C lock(): gets ticket 4, runs
Ticket locks guarantee bounded waiting and fairness — threads are served in FIFO order.
Summary: Lock Implementations Compared
| Implementation | Mutual Exclusion | Progress | Bounded Waiting | Fairness | Performance |
|---|---|---|---|---|---|
| Disable interrupts | Yes (uniprocessor only) | Yes | Yes | N/A | Poor (blocks all interrupts) |
| Load/Store spinlock | No | N/A | N/A | No | N/A |
xchg spinlock | Yes | Yes | No | No | Wastes CPU spinning |
| CAS spinlock | Yes | Yes | No | No | Wastes CPU spinning |
| Ticket lock | Yes | Yes | Yes | Yes (FIFO) | Wastes CPU spinning |
Related
- Navigation: ← Lecture 11 · Lecture 13 →
- Exam: Midterm 2
- Course: CS 537 Map of Content