How Linux keeps tasks safe when they share things.

A class project for Real-Time Operating Systems. Six small demos that you can run with one click. Each demo shows a real problem that happens when many tasks use the same data — and the tool Linux uses to fix it. Every text is in simple English.

mutex spinlock semaphore rw-lock deadlock priority inv.
SUBJECT   Real-Time Operating Systems YEAR   2025–2026 TOPIC   Linux Synchronization Tools
deployed via GitHub Pages
01 · Why

Why do we need this?

In a real-time system, many small tasks run at the same time. They often need to use the same data. If two tasks change the same thing at the same time, the result can be wrong — and you may not even notice. Linux gives us special tools to stop this from happening.

The problem

Adding 1 to a number looks simple, but the computer does it in three steps: read the number, add 1, save the number. If another task runs between step 1 and step 3, your change can be lost. The program does not crash. It just gives wrong numbers, sometimes.

The tools

Linux gives us many tools to keep tasks safe: mutex, spinlock, semaphore, read-write lock, and more. They all help, but each one is good for a different job. Picking the right one matters.

The goal of this project: an app that lets you see these tools working — and see the bugs they fix — so they stop being just words in a book.
02 · Words you need

A small dictionary

Before the demos, here is a short list of the words we will use. They sound hard, but the idea behind each one is simple.

  1. Task / threadA small unit of work the computer runs. Many can run at once.
  2. Race conditionA bug that happens when two tasks change the same thing at the same time and one change is lost.
  3. MutexA "key" that only one task can hold. Other tasks wait their turn (they sleep).
  4. SpinlockThe same idea as a mutex, but waiting tasks keep checking in a loop instead of sleeping. Faster, but uses CPU.
  5. SemaphoreA counter for a shared thing. If the count is zero, the next task has to wait.
  6. Read-Write lockMany tasks can read at the same time. Only one task can write, alone.
  7. DeadlockTwo tasks both wait for each other. Nothing moves. Like two people in a doorway, each saying "you go first".
  8. Priority inversionAn important task gets stuck because of a less important one. A bug.
  9. Priority inheritanceThe fix: the task that holds the key gets a "boost" so it can finish quickly.
03 · How it is built

How does this app work?

Everything runs inside your browser. There is no server. Demos 1 and 2 use Web Workers — small helpers in the browser that act like real tasks running at the same time. They share a piece of memory and use the same kind of "lock" that Linux uses inside.

What you see

The page

  • Dark glass design
  • One file for each demo
  • Works on phone and desktop
Web Worker Shared memory
What runs

Real tasks & locks

  • Two helpers compete for one number
  • Locks make them take turns
  • Same idea as Linux uses

No server

Just a web page. We push the code and GitHub puts it online.

Works anywhere

A small helper makes sure shared memory works in any browser.

Bonus: Python

The same demos also exist as a Python program in legacy_python/, using real Linux threads.

04 · Try it

The demos

Click Run on any card. You will see real numbers from your own computer — not fake ones. Open the box "What is this?" on each card to read what the demo is doing.

01 Race condition

Two helpers, one number. With and without a "key".

What is this?

Two helpers count to a big number. Each helper says "+1" many, many times. The total should be exact.

Without a lock: both helpers sometimes read the same number, add 1, and save it. The other helper's "+1" is lost. The total is too small.

With a mutex (a "key"): only one helper at a time can change the number. The total is correct.

Expected
No mutex
With mutex
Lost updates
Show code real · web workers
// Two real Web Workers run this code in parallel. // Both share one Int32Array via SharedArrayBuffer. // ---- Without a lock — the bug ---- for (let i = 0; i < N; i++) { const v = view[COUNTER]; // read // tiny CPU burn so the OS can switch threads here view[COUNTER] = v + 1; // write — peer's update can be lost } // ---- With a mutex — the fix ---- function mutexLock() { while (Atomics.compareExchange(view, MUTEX, 0, 1) !== 0) { Atomics.wait(view, MUTEX, 1); // sleep until free } } function mutexUnlock() { Atomics.store(view, MUTEX, 0); Atomics.notify(view, MUTEX, 1); // wake one waiter } for (let i = 0; i < N; i++) { mutexLock(); view[COUNTER] += 1; mutexUnlock(); }
Atomics.wait / notify are the JS equivalent of Linux futex(2) — the same syscall pthread_mutex_t uses inside.

02 Mutex vs spinlock

Same job, two ways to wait. Which one is faster?

What is this?

Both mutex and spinlock are "keys" — only one task at a time can hold them. The difference is how the other tasks wait.

Mutex: waiting tasks sleep. They use no CPU. The OS wakes them up when the key is free.

Spinlock: waiting tasks keep checking in a loop. They use CPU but they react instantly.

Spinlocks are good for very short jobs. Mutexes are good for longer ones. Linux uses both. The chart below shows the time each one takes for the same job.

Mutex
— ms
Spinlock
— ms
Show code real · atomic operations
// MUTEX — sleeps when blocked (Linux: futex) function mutexLock() { while (Atomics.compareExchange(view, MUTEX, 0, 1) !== 0) { Atomics.wait(view, MUTEX, 1); // kernel puts us to sleep } } // SPINLOCK — busy-waits, never sleeps function spinLock() { while (Atomics.compareExchange(view, SPIN, 0, 1) !== 0) { // keep checking, burning CPU } } // Same job for both: 30 000 increments × 4 threads. for (let i = 0; i < iterations; i++) { lock(); view[COUNTER] += 1; unlock(); }
compareExchange is one CPU instruction (CMPXCHG on x86). It is atomic in hardware — the same primitive Linux uses.

03 Producer / consumer

One task makes things. Another uses them. They share a small box.

What is this?

A common setup. The producer creates items (numbers). The consumer takes them one by one. Between them is a box (buffer) with a fixed size.

Two rules:

• If the box is full, the producer waits.
• If the box is empty, the consumer waits.

We use two semaphores (counters) and a mutex to make this safe. This is how Linux moves data between parts of the system: from a microphone to a speaker, from the network to your program, etc.

Buffer size
Produced
0
Consumed
0
In flight
0
Show code simulated · same algorithm
// A counting semaphore — same algorithm Linux uses. class Semaphore { constructor(value) { this.value = value; this.waiters = []; } async acquire() { if (this.value > 0) { this.value -= 1; return; } await new Promise((res) => this.waiters.push(res)); } release() { if (this.waiters.length) this.waiters.shift()(); else this.value += 1; } } const empty = new Semaphore(BUFFER_SIZE); // free slots const full = new Semaphore(0); // ready items const mutex = new Mutex(); async function producer() { for (let i = 0; i < N; i++) { await empty.acquire(); // wait if full await mutex.acquire(); buffer.push(i); mutex.release(); full.release(); // signal item ready } }
The algorithm is exactly what sem_wait / sem_post do in Linux. Only the language is different.

04 Readers / writers

Many can read at once. Only one can write at a time.

What is this?

Think of a book in a library. Many people can read it at the same time — that is fine. But if someone wants to write a note in it, they need the book alone for a moment.

That is the read-write lock. It lets many readers work together, but a writer gets the data alone. Linux has this built in.

In this demo, each line shows one task. R1, R2, R3, R4 are readers and W1 is a writer. Watch how they take turns.

Reads
0
Writes
0
Show code simulated · same algorithm
// Writer-priority RWLock — readers cannot starve a waiting writer. class RWLock { constructor() { this.readers = 0; this.writer = false; this.waitingWriters = 0; this.waiters = []; } async acquireRead() { // wait if anyone is writing OR a writer is waiting while (this.writer || this.waitingWriters > 0) { await this._wait(); } this.readers += 1; } releaseRead() { this.readers -= 1; if (this.readers === 0) this._wake(); } async acquireWrite() { this.waitingWriters += 1; while (this.writer || this.readers > 0) { await this._wait(); } this.waitingWriters -= 1; this.writer = true; } releaseWrite() { this.writer = false; this._wake(); } }
Linux exposes the same idea as pthread_rwlock_t.

05 Deadlock

Two tasks. Two keys. Both stuck waiting for each other.

What is this?

Imagine two people, each with one key. Person A has key 1 and wants key 2. Person B has key 2 and wants key 1. Neither one gives up. They wait forever. This is a deadlock.

The fix is simple: agree on an order. Everyone takes key 1 first, then key 2. Now no one can get stuck.

Click Cause to see the bug. (After 2.5 seconds we stop the test, so the page does not freeze.) Then click Fix to see the working version.

Thread A
idle
Thread B
idle
Result
Elapsed
— ms
Show code simulated · same algorithm
// Two threads, two locks, opposite order — classic deadlock. // ---- BROKEN ---- async function threadA() { await lock1.acquire(); // A grabs lock 1 await sleep(50); await lock2.acquire(); // then wants lock 2 — hangs lock2.release(); lock1.release(); } async function threadB_broken() { await lock2.acquire(); // B grabs lock 2 await sleep(50); await lock1.acquire(); // then wants lock 1 — hangs lock1.release(); lock2.release(); } // ---- FIXED — same global order ---- async function threadB_fixed() { await lock1.acquire(); // always lock 1 first! await sleep(50); await lock2.acquire(); lock2.release(); lock1.release(); }
Real Linux kernels add lockdep — a runtime check that warns if any two locks are ever taken in conflicting orders.

06 Priority inversion

An important task gets stuck because of a less important one.

What is this?

Three tasks: Low (small job), Medium (uses CPU a lot), and High (very important).

The bug: Low takes a key. High wakes up and needs the same key, so High waits. Then Medium starts. Medium is more important than Low, so the OS gives the CPU to Medium. Now Low cannot finish, and High keeps waiting because of Medium! The most important task is stuck. This is priority inversion.

The fix (priority inheritance): when High waits for Low, we give Low a temporary "boost". Now Medium cannot push Low aside. Low finishes fast, gives back the key, and High can run. This bug actually happened on the Mars Pathfinder rover in 1997!

Without priority inheritance
High
Medium
Low
With priority inheritance
High
Medium
Low
Latency · no PI
— ms
Latency · with PI
— ms
Show code simulated · cooperative scheduler
// Cooperative scheduler so the inversion is visible. // `usePI` flag turns priority inheritance on or off. function shouldRun(name) { // PI: the lock holder gets the CPU when High is waiting. if (usePI && sched.highWaiting && sched.mutexHolder === name) return true; // Plain: the highest-priority runnable thread wins. return sched.holder === name; } async function low() { sched.mutexHolder = 'Low'; for (let i = 0; i < 12; i++) { while (!shouldRun('Low')) await sleep(5); await sleep(40); // critical section work } sched.mutexHolder = null; } async function high() { await sleep(120); sched.highWaiting = true; if (usePI) claimCPU('Low'); // boost Low while (sched.mutexHolder !== null) await sleep(10); sched.mutexHolder = 'High'; }
Real Linux uses PTHREAD_PRIO_INHERIT, set on the mutex attribute. The scheduler does the boosting — same idea.
05 · What we learned

Four lessons

Building each demo by hand made these ideas easy to feel, not just to read. Here are the four big lessons.

1. There is no "best" lock

Spinlocks are good for very short jobs. Mutexes are good for longer jobs. Linux uses both. You pick the one that fits the job, not "the fastest one".

2. Deadlocks are about rules

A deadlock is not "I forgot a lock". It is "two tasks took locks in a different order". The fix is a simple rule that everyone follows: always take key 1 before key 2.

3. Important tasks need a "boost"

Without priority inheritance, a less important task can block a very important one. This bug nearly broke a NASA mission to Mars in 1997. Linux now has a fix built in.

4. The browser can do real concurrency

Modern browsers can run many tasks at the same time and share memory. The locks we use here work the same way as the ones inside Linux. The ideas are the same; only the language is different.

06 · Code

Where to find everything

All the code, this page, the screenshots and the bonus Python version live in one place on GitHub. Every change is published online automatically.

RTOS · 2025–2026