Synchronization
When multiple threads share data, things can go wrong fast. Without coordination, two threads might update the same variable at the same time, creating a race condition -- the result depends on which thread happens to run first.
Synchronization primitives solve this. They are the traffic lights and turnstiles of concurrent programming.
The Problem: Race Conditions
Imagine two threads both trying to increment a counter:
Thread A reads counter (value: 5)
Thread B reads counter (value: 5)
Thread A writes counter = 6
Thread B writes counter = 6 <-- should be 7!
One increment was lost. This is a data race: both threads read the old value before either wrote the new one.
Mutex (Mutual Exclusion)
Analogy: A bathroom lock.
A mutex is the simplest synchronization tool. Only one thread can hold it at a time. If another thread tries to acquire it, they wait in a queue until the holder releases it.
Thread A: acquire lock -> success, enters critical section
Thread B: acquire lock -> blocked, added to wait queue
Thread A: release lock -> Thread B wakes up and acquires
Thread B: release lock -> lock is free
Key rules: - Only the thread that locked it can unlock it (owner tracking) - Waiters are served in FIFO order (no starvation) - Forgetting to release causes deadlock
Shell usage:
Semaphore (Counting Lock)
Analogy: A parking lot with limited spaces.
A semaphore is like a mutex, but instead of allowing only 1 holder, it allows up to N concurrent holders. Each acquire decrements the count; each release increments it. When the count reaches zero, new arrivals wait.
Semaphore "parking" (count=3)
Car A enters -> count=2
Car B enters -> count=1
Car C enters -> count=0
Car D arrives -> blocked (lot is full)
Car A leaves -> count stays 0, Car D enters
Special cases: - Binary semaphore (count=1): behaves like a mutex - Bounded semaphore: enforces a maximum count to prevent accidental over-releasing
Shell usage:
Condition Variable (Wait/Notify)
Analogy: A waiting room where you sit until your name is called.
A condition variable lets threads wait for some condition to become true. It is always paired with a mutex:
- Thread acquires mutex
- Checks the condition -- if not met, calls
wait waitatomically releases the mutex and puts the thread to sleep- Another thread changes the data, then calls
notifyornotify_all - The waiting thread wakes up and re-acquires the mutex
This is the standard producer-consumer pattern: the producer adds items and notifies; the consumer waits until items are available.
Reader-Writer Lock (RWLock)
Analogy: A museum exhibit.
Imagine a famous painting in a museum. Any number of visitors (readers) can look at the painting at the same time -- they don't interfere with each other. But when a restorer (writer) needs to work on the painting, they close the room. Visitors already inside can finish looking, but no new visitors are let in until the restorer is done.
A reader-writer lock works the same way: - Multiple readers can hold the lock at the same time - Only one writer can hold the lock, and nobody else (no readers, no other writers) - This is perfect for data that gets read much more often than it gets written (config files, caches, lookup tables)
Thread A: acquire_read -> success (1 reader)
Thread B: acquire_read -> success (2 readers)
Thread C: acquire_write -> blocked, added to wait queue
Thread D: acquire_read -> blocked behind writer (writer-preference!)
Thread A: release_read -> nothing happens (B still reading)
Thread B: release_read -> 0 readers, C wakes up (writer goes next)
Thread C: release_write -> D wakes up (reader gets in)
Writer-Preference
Notice that Thread D was blocked even though it was a reader. This is writer-preference: when a writer is waiting in line, new readers queue behind it rather than jumping ahead. Without this rule, a steady stream of readers could starve the writer forever -- the writer would never get a turn.
Caveat: unlike the mutex and semaphore (which serve waiters in FIFO order and are starvation-free), the ReadWriteLock is not starvation-free for readers. It deliberately favours writers, so a steady stream of writers can keep new readers waiting indefinitely -- a trade-off you accept in exchange for never starving writers.
Shell usage:
Priority Inversion
Analogy: Three students and one textbook.
Imagine a classroom with three students who all need the same textbook:
- Slow Student (low priority) -- currently has the textbook and is reading
- Fast Student (high priority) -- needs the textbook urgently
- Normal Student (medium priority) -- doesn't need the textbook at all
The teacher (scheduler) always calls on the highest-priority student who is ready to work. Here's the problem:
- Slow Student has the textbook and is reading chapter 3
- Fast Student raises their hand -- "I need that textbook!" But Slow Student still has it
- Normal Student doesn't need the textbook, so they're ready to work
- The teacher keeps calling on Normal Student (priority 5) instead of Slow Student (priority 1)
- Slow Student never gets a turn, so they can never finish and return the textbook
- Fast Student is stuck forever!
This is priority inversion: the high-priority task is effectively running at the lowest priority because it's blocked behind the low-priority holder, and medium-priority tasks keep jumping ahead. In other words, the important task gets stuck waiting because a less important task is hogging the lock.
The Mars Pathfinder Bug
This exact problem happened on Mars in 1997. NASA's Pathfinder rover had three tasks:
- A low-priority task that collected weather data (and held a shared mutex)
- A high-priority task that managed the communication bus
- Medium-priority tasks that ran science experiments
The high-priority bus manager kept getting starved. A watchdog timer detected the failure and rebooted the rover -- over and over. Engineers on Earth diagnosed the problem and uploaded a fix: priority inheritance.
Shell usage:
pi demo # Walk through the Mars Pathfinder scenario step by step
pi status # See which processes are currently boosted
Priority Inheritance
The fix: temporarily boost the slow student.
Priority inheritance is the kernel's solution to priority inversion. When a high-priority thread blocks on a mutex held by a lower-priority thread, the kernel temporarily boosts the holder's priority to match the waiter's.
Back to our classroom:
- Fast Student says "I need the textbook"
- The teacher sees Fast Student is blocked waiting for Slow Student
- The teacher says "Slow Student, you're temporarily promoted to priority 10!"
- Now the teacher calls on Slow Student (priority 10) instead of Normal Student (priority 5)
- Slow Student finishes quickly, returns the textbook
- Slow Student drops back to priority 1
- Fast Student gets the textbook and proceeds
Transitive Inheritance
What if Slow Student is also waiting for something? The boost propagates through the chain:
Fast Student (priority=10) blocked on Textbook, held by Normal Student (priority=5)
Normal Student also blocked on Calculator, held by Slow Student (priority=1)
Result: Slow Student boosted to 10, Normal Student boosted to 10
When Slow Student returns Calculator -> Normal Student gets it, Slow Student drops to 1
When Normal Student returns Textbook -> Fast Student gets it, Normal Student drops to 5
The kernel walks the chain of "who is blocked on what" and boosts everyone in the path. Loop detection (via a visited set) prevents infinite chains.
How It Works
Each process has two priority values:
- Base priority -- the original, immutable priority set at creation
- Effective priority -- what the scheduler actually uses (may be boosted)
When no inheritance is active, effective_priority == base_priority. When a boost happens, only the effective priority changes. When the mutex is released, the kernel recalculates: it looks at all mutexes the process still holds, finds the highest-priority waiter across all of them, and sets effective_priority = max(base_priority, max_waiter_priority).
Deadlock
Deadlock is the nightmare scenario: two or more processes are stuck forever, each holding a resource the other needs. Nobody can move. It's like two people in a narrow hallway, each refusing to step aside.
The Four Coffman Conditions
In 1971, computer scientists Coffman, Elphick, and Shoshani proved that deadlock can only happen when all four of these conditions hold at the same time:
- Mutual exclusion -- a resource can only be used by one process at a time (like a bathroom with a lock).
- Hold and wait -- a process can hold resources while waiting for more (like a student holding one textbook while asking for another).
- No preemption -- you can't forcibly take a resource from someone (no grabbing the textbook out of their hands).
- Circular wait -- A waits for B, B waits for A, forming a circle (like two people at a revolving door, each waiting for the other to go first).
Break any one of these and deadlock becomes impossible.
Deadlock Prevention: Resource Ordering
PyOS prevents deadlock by attacking the circular wait condition using resource ordering.
Analogy: Numbered lockers in a school hallway. The rule: you can only walk forward. If you need locker 3 and locker 7, open 3 first, then walk forward to 7. You can never go backwards. Nobody ever gets stuck in a circle.
Here's why this works: imagine you need lockers 3 and 7, and your friend needs lockers 7 and 3. With the "always go forward" rule:
- You open locker 3 first, then walk to 7.
- Your friend also opens locker 3 first (they can't start at 7!), then walks to 7.
- No circle is possible because everyone walks the same direction.
In PyOS, every resource (mutex, semaphore, reader-writer lock) gets a numeric rank. Before a process acquires a resource, the kernel checks: "Is this rank higher than everything I already hold?" If yes, allowed. If no, it's a violation.
Three Modes
| Mode | What happens on violation |
|---|---|
| strict | Reject the acquire -- the process can't get the resource |
| warn | Allow it, but record the violation for debugging |
| off | No checking at all (maximum performance) |
Shell usage:
ordering mode strict
ordering register mutex:lock_a 1
ordering register mutex:lock_b 2
ordering status
ordering violations
ordering demo
Prevention vs. Detection
PyOS has two complementary approaches to deadlock:
| Aspect | Prevention (ordering) | Detection (Banker's algorithm) |
|---|---|---|
| When | Before deadlock happens | After deadlock happens |
| How | Structural rule (rank ordering) | Periodic scan (safety check) |
| Overhead | One comparison per acquire | Full matrix computation |
| Guarantee | Deadlock impossible | Deadlock found and reported |
| Trade-off | May reject valid acquires | Doesn't prevent deadlock |
| Module | sync/ordering.py |
sync/deadlock.py |
Think of it this way: ordering is like a one-way street (prevents collisions), while detection is like a traffic camera (catches collisions after they happen). Best practice is to use both: ordering prevents most deadlocks cheaply, and detection catches any that slip through.
How It Works in PyOS
PyOS implements all four primitives in sync/primitives.py, plus priority
inheritance in sync/inheritance.py and deadlock prevention in
sync/ordering.py:
| Class | Purpose | Key Methods |
|---|---|---|
Mutex |
Mutual exclusion | acquire(tid), release(tid), waiters |
Semaphore |
Counting lock | acquire(tid), release() |
Condition |
Wait/notify | wait(tid), notify(), notify_all() |
ReadWriteLock |
Multiple readers / one writer | acquire_read(tid), acquire_write(tid), release_read(tid), release_write(tid) |
SyncManager |
Registry | create_mutex(), create_semaphore(), create_condition(), create_rwlock() |
PriorityInheritanceManager |
Prevent priority inversion | on_acquire(), on_block(), on_release() |
ResourceOrderingManager |
Prevent deadlock (resource ordering) | register(), check_acquire(), on_acquire(), on_release() |
The kernel owns a SyncManager, a PriorityInheritanceManager, and a ResourceOrderingManager, all created during boot and torn down during shutdown. All user-space access goes through system calls (91-93, 110-125), and the shell provides mutex, semaphore, rwlock, pi, and ordering commands.
The PriorityInheritanceManager tracks which process holds each mutex, which processes are blocked waiting, and coordinates priority boosts. The Mutex itself stays simple -- all coordination logic lives in the PI manager.
Syscall Interface
| Number | Name | What It Does |
|---|---|---|
| 110 | SYS_CREATE_MUTEX | Create a named mutex |
| 111 | SYS_ACQUIRE_MUTEX | Lock a mutex (or queue if held); optional pid enables PI tracking |
| 112 | SYS_RELEASE_MUTEX | Unlock a mutex; optional pid triggers priority recalculation |
| 113 | SYS_CREATE_SEMAPHORE | Create a counting semaphore |
| 114 | SYS_ACQUIRE_SEMAPHORE | Decrement semaphore (or queue if zero) |
| 115 | SYS_RELEASE_SEMAPHORE | Increment semaphore |
| 116 | SYS_CREATE_CONDITION | Create a condition variable |
| 117 | SYS_CONDITION_WAIT | Wait on a condition |
| 118 | SYS_CONDITION_NOTIFY | Wake waiters on a condition |
| 119 | SYS_CREATE_RWLOCK | Create a reader-writer lock |
| 122 | SYS_ACQUIRE_READ_LOCK | Acquire read access (or queue) |
| 123 | SYS_ACQUIRE_WRITE_LOCK | Acquire write access (or queue) |
| 124 | SYS_RELEASE_READ_LOCK | Release read access |
| 125 | SYS_RELEASE_WRITE_LOCK | Release write access |
No new syscall numbers were added for priority inheritance -- the existing SYS_ACQUIRE_MUTEX (111) and SYS_RELEASE_MUTEX (112) accept an optional pid parameter that activates PI tracking.
Deadlock prevention syscalls:
| Number | Name | What It Does |
|---|---|---|
| 91 | SYS_CHECK_ORDERING | Return ordering status (mode, ranks, violations) |
| 92 | SYS_SET_ORDERING_MODE | Set enforcement mode (strict/warn/off) |
| 93 | SYS_REGISTER_RANK | Register a resource with an explicit rank |
Real-World Examples
- Mutex: Protecting a shared log file so lines don't interleave
- Semaphore: Limiting database connections to a pool of 10
- Condition: A print queue where the printer waits for jobs to arrive
- Reader-writer lock: A configuration file that many threads read but only one thread updates
- Priority inheritance: The Mars Pathfinder rover fix -- preventing high-priority tasks from starving when they need a mutex held by a low-priority task
Where to Go Next
With synchronization in place, you have the tools to build higher-level patterns like thread-safe queues and barriers. These primitives are the building blocks of every concurrent system, from web servers to operating systems.
- Processes -- How threads live inside processes, plus fork, signals, and wait/waitpid
- Users and Safety -- Deadlock detection and how the OS keeps things safe
- Devices and Networking -- IPC pipes and message queues that use these primitives under the hood
- The Shell -- The
mutex,semaphore, andrwlockshell commands
Key Terms
| Term | Definition |
|---|---|
| Mutex | A lock that only one thread can hold at a time -- mutual exclusion |
| Semaphore | A counter-based lock that allows up to N threads to enter at once |
| Condition variable | A waiting room where threads sleep until another thread signals them |
| Reader-writer lock | A lock that allows many readers or one writer, but not both |
| Race condition | A bug where two threads access shared data at the same time and produce unpredictable results |
| Critical section | The part of code that accesses shared data and must be protected |
| Deadlock | Two or more threads stuck forever, each waiting for a lock the other holds |
| Priority inversion | When a high-priority thread is blocked because a low-priority thread holds its lock |
| Priority inheritance | Temporarily boosting a lock-holder's priority so higher-priority waiters are not starved |