Skip to content

← Fearless Concurrency: Threads, Channels, Shared State step 12 of 24

Hard Framework

Release a read guard before acquiring a write lock

Repair the locking logic in a shared accumulator. Each worker reads an RwLock<Option<u64>>, releases its read guard, then takes a write guard to update the value.

pub fn get_or_init_shared(initial: Option<u64>, ops: Vec<u64>, workers: usize) -> u64

Start the shared cell at initial. For each operation x, initialize an empty cell to Some(x), or add x to its existing value. Process every operation exactly once. Return the final value, using 0 if the cell is still empty. Treat zero workers as one.

Keep the supplied thread setup, chunking and joins. Replace the starter's wildcard read with a boolean recording whether the cell was empty. Release the read guard before requesting the write lock, then check the cell again under the write guard before initializing it.

Inputs have a total that fits in u64. Since all additions are nonnegative, the expected result is initial.unwrap_or(0) + sum(ops) regardless of worker order. For example, None with [1, 2, 3, 4] returns 10; Some(100) with [1, 2, 3] returns 106. Empty operations return the initial value or zero.

When a guard releases its lock

Dropping a lock guard releases the lock. The starter writes:

let _ = shared.read().expect("lock poisoned");

This creates a temporary guard and drops it at the end of the statement. It does not protect the later update. Rust's deny-by-default let_underscore_lock lint rejects this spelling.

Changing _ to _guard creates a named binding that normally lives to the end of its scope. That is also wrong here: requesting a write lock while still holding the read guard can deadlock or panic. It is not a supported way to upgrade a standard-library read lock.

Instead, copy out the observation and let the temporary guard drop:

let needs_init = shared.read().expect("lock poisoned").is_none();
// The read guard is gone; needs_init is an owned bool.
let mut w = shared.write().expect("lock poisoned");

This statement creates a new temporary. Do not generalize it to let _ = existing_guard;: a wildcard does not move and drop an existing named guard. Use drop(existing_guard) or a smaller scope when you need to release that guard explicitly.

Recheck after acquiring the write lock

Another worker can update the cell after your read guard drops. needs_init describes an earlier observation, not necessarily the current state. Initialize only if the cell is still empty under the write lock; otherwise add to the value now stored there. Using a stale observation to overwrite the cell would lose another worker's contribution.

The read phase is a learning requirement here, not a performance fast path: every operation still takes the write lock. For this accumulator alone, a single write-lock acquisition would be simpler. The exercise practices how to release a read guard and revalidate an observation before updating.

Temporary scope matters

A guard created in a match scrutinee can remain alive through the selected arm. Trying to acquire the same lock inside that arm can block progress.

Rust edition 2024 shortens a temporary's scope in an if let condition when the pattern fails: it is dropped before entering else. A successful match can still keep the guard alive through the then branch. This change does not make every lock acquisition inside an if let safe. A separate statement or explicit scope makes the intended release point easier to see.

Also release guards before joining workers that need the same lock. A join waits for the worker to finish; holding its required lock while waiting prevents that from happening.

A timeout can indicate a deadlock, but is not proof of one. Inspect guard scopes, lock acquisition order and which workers each join waits for.

Loading visualization…