---
title: 'Mutex and Read-Write Lock'
source: 'https://academia.sh/en/courses/rust-concurrency/mutex-and-rwlock'
course: 'Concurrency and Asynchronous Rust'
language: en
updated: '2026-09-15T04:58:44+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures with a channel handshake, in a deterministic way, that the side holding the lock advances on its own and the side waiting for it advances when something else finishes'
    - 'Shows that which of two waiting sides enters first is not measured, and why it is not measured'
    - 'Verifies by running it that the read side of a read-write lock allows more than one reader at the same time'
---

# Mutex and Read-Write Lock

The side holding a mutex advances on its own, the side waiting for it advances when something else finishes; this order is made deterministic with a channel handshake, and the read side of a read-write lock departs from a mutex by allowing more than one reader at the same time.

The previous lesson showed two threads operating in sequence on a shared integer, but showed
that the program itself forced that order through join calls: `second` never started before
`first` had completely finished. When these two operations need to run at the same time and
only need to fall into sequence at the moment they access the shared data, joining is not a
sufficient tool — the rest of the two threads can advance in parallel, and only the moments
they enter the critical section need to fall into sequence. This lesson measures directly how
a waiting side advances through a lock.

This lesson runs two different kinds of lock: `Mutex`, a mutex that allows only one side to
access at a time; `RwLock`, a read-write lock that handles read and write access separately.
Both offer a solution to the same basic problem — making concurrent access to shared data safe
— but they differ in the degree of sharing they permit. The lesson measures this difference
with three separate scenarios: the lock holder advancing on its own, the waiting side advancing
when something else finishes, and how the read side of the read-write lock loosens this rule.

## The Lock Holder On Its Own, the Waiter When Something Else Finishes

The concept of a mutex is not established again here. The Operating System Concepts course
established what a lock is for; the JVM, Concurrency and Performance course measured the
identity of the thread holding a lock. This course asks a different question: what the side
**holding** the lock and the side **waiting** for the lock need in order to advance. The answer
is short: a `lock` call made while the lock is free returns needing no one — it falls into the
on-its-own class. A `lock` call made while the lock is held, on the other hand, waits until the
lock holder releases it — it falls into the when-something-else-finishes class. This has the
same structure as the `recv` call in the Message Passing with Channels lesson: both wait for an
event outside themselves to occur, and the difference is only whether the awaited event is a
send or a release.

The lock itself is represented by a handle the `lock` call returns. When this handle goes out
of scope (or is explicitly dropped), the lock is automatically released — an extension of the
drop rule established in the Ownership and the Memory Model course. In the program below, the
handle named `entry` inside the `holder` thread keeps the lock in hand until the function's
body finishes and it goes out of scope; when the lock is released depends not on an explicit
call from the programmer, but on where this handle's life ends. This is the same mechanism as
the ownership rule for a moved value seen in the previous lesson — here it is applied to a lock
handle.

This relationship has to be measured without looking at the runtime's actual timing, because
timing depends on the environment and is never printed in this course. The program below proves
with a channel handshake that the lock holder genuinely touches the lock, and that the waiting
side genuinely enters afterward: the waiting side does not even start trying before it receives
a signal that the holder has entered the lock, and the order of the records both sides write to
a shared ledger is forced by the lock itself. Without this handshake — that is, if `waiter` made
its own lock call without waiting for any signal — the measurement would still produce the
correct order, because the lock already gives an absolute order guarantee; but in that case it
would remain unclear whether `waiter`'s call was actually made while `A` held the lock, or
before that. The handshake makes clear **which moment** the measurement is proving.

## A Deterministic Order Through a Handshake

The program below runs three separate scenarios back to back, all three isolated within the
same `main` function with their own channels. The first scenario measures a single waiter and
uses two separate signal channels: `a_entered_sender`/`a_entered_receiver` tells `waiter` that
`holder` has entered the lock; `proceed_sender`/`proceed_receiver` tells `holder` that `waiter`
has started trying. This two-way handshake pins down which side touches the lock when — the
lock itself (`log`) knows nothing of this signaling, it is protected only by the standard
`lock` call.

```rust
// lock.rs — the lock holder advances on its own, the lock waiter when something else finishes.

use std::sync::mpsc::channel;
use std::sync::{Arc, Mutex, RwLock};
use std::thread;

fn main() {
    // --- exit order is made deterministic with a handshake ---
    let log = Arc::new(Mutex::new(Vec::<String>::new()));
    let (a_entered_sender, a_entered_receiver) = channel::<()>();
    let (proceed_sender, proceed_receiver) = channel::<()>();

    let g = Arc::clone(&log);
    let holder = thread::spawn(move || {
        // on its own: the lock was free, taken without waiting for anyone
        let mut entry = g.lock().unwrap();
        entry.push(String::from("A-entered"));
        a_entered_sender.send(()).unwrap();
        proceed_receiver.recv().unwrap(); // held until B is confirmed to have started trying
        entry.push(String::from("A-exited"));
        // entry goes out of scope and is dropped here
    });

    a_entered_receiver.recv().unwrap();
    let g = Arc::clone(&log);
    let waiter = thread::spawn(move || {
        proceed_sender.send(()).unwrap();
        // when something else finishes: blocked here until A releases the lock
        let mut entry = g.lock().unwrap();
        entry.push(String::from("B-entered"));
        entry.push(String::from("B-exited"));
    });

    holder.join().unwrap();
    waiter.join().unwrap();

    println!("single-waiter order: {:?}", *log.lock().unwrap());

    // --- two waiters: which enters first is not measured, only that both entered ---
    let log2 = Arc::new(Mutex::new(Vec::<String>::new()));
    {
        let mut first = log2.lock().unwrap();
        first.push(String::from("start"));
    }

    let g = Arc::clone(&log2);
    let c = thread::spawn(move || {
        let mut entry = g.lock().unwrap();
        entry.push(String::from("C-entered"));
    });
    let g = Arc::clone(&log2);
    let d = thread::spawn(move || {
        let mut entry = g.lock().unwrap();
        entry.push(String::from("D-entered"));
    });
    c.join().unwrap();
    d.join().unwrap();

    let mut entrants: Vec<String> = log2
        .lock()
        .unwrap()
        .iter()
        .filter(|s| s.ends_with("-entered"))
        .cloned()
        .collect();
    entrants.sort();
    println!("records entered by the two waiters (order not measured): {:?}", entrants);

    // --- read-write lock: more than one reader can hold the lock at once ---
    let value = Arc::new(RwLock::new(42));
    let (r1_entered_sender, r1_entered_receiver) = channel::<()>();
    let (r2_done_sender, r2_done_receiver) = channel::<()>();
    let (release_sender, release_receiver) = channel::<()>();

    let v1 = Arc::clone(&value);
    let reader1 = thread::spawn(move || {
        let handle = v1.read().unwrap();
        r1_entered_sender.send(()).unwrap();
        release_receiver.recv().unwrap();
        *handle
    });

    r1_entered_receiver.recv().unwrap();
    let v2 = Arc::clone(&value);
    let reader2 = thread::spawn(move || {
        // this call also successfully takes the lock while R1 still holds the read lock
        let handle = v2.read().unwrap();
        let result = *handle;
        r2_done_sender.send(()).unwrap();
        result
    });

    r2_done_receiver.recv().unwrap();
    release_sender.send(()).unwrap();

    let result1 = reader1.join().unwrap();
    let result2 = reader2.join().unwrap();

    println!();
    println!("both readers held the lock at once, values read: {} and {}", result1, result2);
    println!("multiple readers were allowed: {}", result1 == result2);
}
```

```
single-waiter order: ["A-entered", "A-exited", "B-entered", "B-exited"]
records entered by the two waiters (order not measured): ["C-entered", "D-entered"]

both readers held the lock at once, values read: 42 and 42
multiple readers were allowed: true
```

**TH6.** The four-entry order in the first line comes out identical across three separate runs:
`A-entered, A-exited, B-entered, B-exited`. This order comes not from timing but from the
program's structure. The `waiter` thread never makes its own lock call before receiving the
signal (`a_entered_receiver.recv()`) that `holder` has entered the lock; `holder`, in turn,
writes its own record (`A-entered`) and sends the signal, and only **after** that waits for a
confirmation (`proceed_receiver.recv()`) that `waiter` has started trying, writing its second
record (`A-exited`) and releasing the lock only once that confirmation arrives. This three-step
handshake guarantees that `waiter`'s lock call is genuinely made while `A` still holds the lock
— `waiter` is blocked at that call, unable to advance until `holder` releases the lock. The
blocking duration is not measured, only the order of the result: `B` entries never appear
before `A`'s exit.

## Two Waiters: Order Is Not Measured

In the second scenario, `C` and `D` wait on the same lock. Both eventually enter — the output
proves that — but **which one entered first** is never measured anywhere. The program does not
hide this uncertainty; instead it builds it into its structure: the records are sorted before
they are collected, exactly as the log channel did in the Thread Creation lesson. The
`entrants` list comes out as `["C-entered", "D-entered"]` on every run — this is the result of
alphabetical sorting, not of the actual entry order.

In this scenario, a `"start"` record is written to the log before `C` and `D` begin, and this
record does not appear in the reported list, because `entrants` filters for only the lines
ending in `-entered`. This filtering is deliberate: the exact order of the records `log2` holds
is in fact known and deterministic (`start` is always first), but the relative order between
`C-entered` and `D-entered` does not carry that certainty. The filtering separates what can be
measured from what cannot within the same data structure.

## The Read-Write Lock: Multiple Readers Allowed

The third scenario directly shows the difference between a mutex and a read-write lock.
`reader1` sends a signal while holding the read lock; `reader2`, **after** receiving that
signal, requests its own read lock on the same value. If this were a mutex, `reader2` would be
blocked until `reader1` released the lock — and since `reader1` is waiting for `reader2` to
send a confirmation, that confirmation would never arrive, and the program would deadlock.
Since the program completes without deadlocking, `reader2`'s read-lock call must genuinely have
returned successfully while `reader1` still held the lock.

This inverts the same logic as the handshake in the first scenario. There, the handshake was
used to prove a wait: to show that `waiter` genuinely requested the lock while `holder` held
it. Here, the handshake is used to prove the **absence** of a wait: if the read calls blocked
each other, the program could never complete this handshake and would wait forever. The program
finishing normally is, on its own, proof that the read side of a read-write lock is shareable —
the most indirect of the measurement methods used in this lesson, but also the most conclusive.

**TH7.** The value the two readers read is exactly identical (`42` and `42`), because both
read the same shared number and no write occurs during the reading. This measurement shows
that the read side of a read-write lock is shareable **within itself**: two readers can hold
the lock at the same time, whereas if the same scenario were set up with a `Mutex`, the program
would deadlock, because `reader1` could never send its confirmation without `reader2` being
able to request its own lock.

The source of this difference is that a read-write lock offers two separate entry points. A
mutex carries a single call — whoever calls it is subject to the same rule. A read-write lock
carries separate calls for reading and writing: the read call accepts as many readers as
request it, as long as no writer holds the lock; the write call, by contrast, always insists on
staying **alone**, sharing with neither another writer nor a reader. Only the read side was run
in this measurement; the write call's behavior is identical to a mutex's — once a writer enters
the lock, every side waiting on that lock at that moment, reader or writer, waits for it to
finish.

## The Limits of the Measurement

A lock protects **data**, it does not establish **order**. A mutex, or the write side of a
read-write lock, guarantees that only one side enters the critical section at a time — this is
what is measured in this lesson. But **which** of two waiters enters first is not part of the
lock's own contract; that is a detail that depends on the operating system's scheduler, the
lock's internal implementation, and the load at that moment. In the `C` and `D` example, this
was deliberately neither measured nor printed — by sorting the records before they are
collected, the question of which order actually occurred was removed entirely from the
program's output.

This boundary also clarifies the difference between a mutex and a read-write lock: the read
side of a read-write lock grants **permission** for more than one waiter to enter at once, but
that permission carries no order guarantee either — if a third reader had been added, when
that reader entered still would not be measured. The only problem a lock solves is when access
to the shared data is **safe**; it never solves the question of which side accesses it first.

This distinction is not an arbitrary choice, it is the natural boundary of the guarantee a lock
provides. A lock defines how many sides can be in the critical section at once (one for a
mutex, unlimited for the read side of a read-write lock); it does not define what determines
the **order** of entry into the critical section, because that is generally a decision left to
the operating system scheduler's own policy. No scheduler policy is named anywhere in this
course, and none will be; what is measured is only the guarantees a lock **gives** and **does
not give**.

## Summary

- The side holding the lock advances on its own; the side waiting for the lock advances when
  something else finishes, until the lock is released. This lesson proved that relationship in
  a deterministic way with a channel handshake.
- Which of two waiting sides enters first is neither measured nor printed; sorting the records
  before they are collected removes this uncertainty from the program's output.
- The read side of a read-write lock allows more than one reader at the same time — this was
  compared against a situation where the same scenario would deadlock with a mutex.
- A lock protects access to shared data, but gives no guarantee about which side enters first;
  these two things are independent of each other.
- The JVM, Concurrency and Performance course measured which thread held a lock; this course
  measures what the side waiting for that same lock needs in order to advance.

## Next Step

A lock stops the waiting side, because access to shared data has to be granted in sequence. In
all three scenarios seen in this lesson, one side had to wait for the lock — either because the
lock was held (`waiter`, `C`, `D`), or because a write request would have been involved (the
write side of a read-write lock). But not every sharing needs protection this strict —
especially when the shared value is as simple as a single number, and the operation performed
on it is small enough for the hardware itself to carry out in a single step. The next lesson
measures a form of sharing that no one waits for at all: a counter advancing lock-free, with
atomic operations, needing no one — and whether that gives the same result as a locked counter.
