ECE344 Fall 2026 (Sec 1) Lec 16 - Locks
Watch on YouTube →
Overview
Jon Eyolfson explains how data races arise when threads update shared memory and how mutexes protect critical sections. Using an eight-thread counter that should reach 8 million, he models `counter++` as a load, add, and store; explores possible interleavings and GCC GIMPLE; then shows why a plain-C check-then-set lock is unsafe and wastes CPU while spinning.
Key takeaways
- A data race requires concurrent access to the same memory location with at least one write; concurrent reads without a writer do not create the described race.
- A global increment is not inherently one atomic action: its load, arithmetic, and store can interleave with another thread's increment and lose an update.
- A mutex makes its protected critical section mutually exclusive, but correctness depends on placing the lock around every access that participates in the shared-data invariant.
- A lock implementation that separately checks a state and sets it is unsafe: two threads can both observe the unlocked value before either claims it.
- Busy-waiting burns CPU while the owning thread must make progress, so a lock must be evaluated for safety, liveness, bounded waiting, and overhead.
- GCC GIMPLE exposes the memory reads and writes behind source-level operations, making it a useful tool for analyzing race conditions and interleavings.
Chapters
0:00
The Lost Increments Behind the 8-Million Counter
- Eight threads each increment a shared counter one million times, but an unlocked run can finish below the expected 8 million.
- A global `counter++` is a load, an add in a thread-local register, and a store, so concurrent updates can overwrite one another.
- The lost update motivates using a lock to protect shared data.
8:00
Data Races, Atomic Operations, and GCC GIMPLE
- A data race occurs when two concurrent accesses target the same memory location and at least one access is a write; concurrent reads alone are safe.
- An atomic operation is indivisible: it completes entirely or does not occur, while a context switch may happen between separate operations.
- GCC GIMPLE breaks a global increment into a memory read, an addition, and a memory write; LLVM provides a comparable intermediate representation.
- Three-address code makes memory accesses easier to analyze than source-level `++`, though real compilers and CPUs may reorder operations.
14:30
Enumerating Interleavings for Two Counter Increments
- With two threads and one increment each, the final counter can be 2 if one thread completes before the other reads.
- If both threads read 0 before either writes, both store 1 and one increment is lost.
- There are six possible orderings of the two reads and two writes when each thread reads before writing; only the orderings that avoid stale reads produce 2.
- Enumerating even this small case shows why reasoning about realistic concurrent programs is difficult.
17:30
Using a POSIX Mutex to Protect a Critical Section
- A mutex provides mutual exclusion: only one thread can execute code between its lock and unlock calls at a time.
- POSIX threads support static initialization with `PTHREAD_MUTEX_INITIALIZER` and dynamic initialization with `pthread_mutex_init`, followed by `pthread_mutex_destroy` when finished.
- `pthread_mutex_lock` blocks when another thread owns the mutex; `pthread_mutex_trylock` instead returns an error if it cannot acquire the lock immediately.
- Protecting the counter increment prevents overlapping updates and makes the result correct.
23:00
Choosing Critical-Section Size and Lock Placement
- Locking around only `counter++` is correct, but locking and unlocking for every one of 8 million increments adds substantial overhead.
- Holding the lock around a thread's entire million-increment loop can reduce lock operations to roughly eight, but serializes those loops.
- Work outside the critical section can still run in parallel; the protected region should be large enough to be correct but no larger than necessary.
- Locking does not come free, so performance depends on the critical-section size and should be measured in performance-focused work.
28:15
Lock Correctness: Safety, Progress, and Bounded Waiting
- Safety means mutual exclusion: at most one thread may be in the critical section at a time.
- Liveness means a thread can make progress when it reaches the lock; a design that blocks every thread is not useful.
- Bounded waiting, or starvation freedom, means a waiting thread eventually acquires the lock rather than being repeatedly bypassed.
- A practical lock should also minimize overhead, avoid wasting resources while waiting, treat threads fairly, and be difficult to misuse.
32:00
Why Disabling Interrupts Is Not a General User-Space Lock
- On an older single-CPU system, a kernel could disable interrupts to prevent timer-driven context switches during a critical section.
- Disabling interrupts does not stop other cores, so the approach does not provide mutual exclusion on a multiprocessor.
- User-mode programs cannot disable hardware interrupts; that capability is reserved for the kernel.
- Synchronization is layered: hardware atomic operations support primitives such as mutexes, which applications use to avoid data races.
33:00
The Plain-C Check-Then-Set Lock Fails
- The attempted lock uses an integer with 0 for unlocked and 1 for locked, spinning while the value is 1 before setting it to 1.
- The check and the store are separate memory operations, so two threads can both read 0, both write 1, and both return as lock owners.
- Because both threads can enter the critical section, the attempted lock fails safety and does not fix the counter race.
- The waiting thread also busy-waits, consuming CPU time; correct acquisition requires the check and ownership update to be atomic.
- A later lecture will address the failed implementation; the key analysis method is to trace possible interleavings of memory reads and writes.
Summary, takeaways, and chapters were generated by AI from the video's transcript and may contain errors. The video belongs to its creator, Jon Eyolfson.