Concurrency · about 45 min

Shared State: Arc and Mutex

Why this matters

Message passing covers many designs, but sometimes several threads genuinely need the same data: a counter, a cache, a shared map of results. Shared mutable state is exactly where data races come from: two threads reading and writing the same memory without coordination produce corrupted, unpredictable results. Rust's answer is two types used together: Mutex<T> makes sure only one thread at a time accesses the data, and Arc<T> lets several threads own the mutex. The compiler refuses to let you share data across threads any other way.

The problem: a data race (that Rust will not compile)

use std::thread;

fn main() {
    let mut count = 0;
    let mut handles = vec![];
    for _ in 0..4 {
        handles.push(thread::spawn(|| {
            count += 1;
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    println!("{count}");
}
error[E0373]: closure may outlive the current function, but it borrows `count`, which is owned by the current function
 --> main.rs:7:36
  |
7 |         handles.push(thread::spawn(|| {
  |                                    ^^ may outlive borrowed value `count`
8 |             count += 1;
  |             ----- `count` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> main.rs:7:22
  |
7 |           handles.push(thread::spawn(|| {
  |  ______________________^
8 | |             count += 1;
9 | |         }));
  | |__________^
help: to force the closure to take ownership of `count` (and any other referenced variables), use the `move` keyword
  |
7 |         handles.push(thread::spawn(move || {
  |                                    ++++

error[E0499]: cannot borrow `count` as mutable more than once at a time
 --> main.rs:7:36
  |
7 |           handles.push(thread::spawn(|| {
  |                        -             ^^ `count` was mutably borrowed here in the previous iteration of the loop
  |  ______________________|
  | |
8 | |             count += 1;
  | |             ----- borrows occur due to use of `count` in closure
9 | |         }));
  | |__________- argument requires that `count` is borrowed for `'static`

error[E0502]: cannot borrow `count` as immutable because it is also borrowed as mutable
  --> main.rs:14:15
   |
7  |           handles.push(thread::spawn(|| {
   |                        -             -- mutable borrow occurs here
   |  ______________________|
   | |
8  | |             count += 1;
   | |             ----- first borrow occurs due to use of `count` in closure
9  | |         }));
   | |__________- argument requires that `count` is borrowed for `'static`
...
14 |       println!("{count}");
   |                 ^^^^^^^ immutable borrow occurs here
   |

Beware the first help: here: adding move makes it compile, but wrongly. count is an i32, which is Copy, so each thread would get its own private copy, increment that, and main would print 0. The real problem is sharing, and move does not share.

In C, the equivalent compiles, and count += 1 (load, add, store) from several threads loses updates: two threads load 5, both store 6. Rust rejects it: four closures would need mutable access to one variable at once. We need a way to coordinate access, and a way for several threads to own that coordinator.

Mutex: one at a time

A Mutex<T> (mutual exclusion) wraps a value. To touch the value you must lock the mutex; only one thread can hold the lock at a time, and others wait:

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(vec![1, 2, 3]);
    {
        let mut guard = m.lock().unwrap();
        guard.push(4);
        println!("inside: {:?}", *guard);
    }
    m.lock().unwrap().push(5);
    println!("after: {:?}", m.lock().unwrap());
}
inside: [1, 2, 3, 4]
after: [1, 2, 3, 4, 5]

Arc: shared ownership across threads

To give each thread access to the same mutex, the mutex needs several owners: Rc, but thread-safe. That is Arc<T> (atomically reference counted), which updates its count with atomic CPU instructions:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let count = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    for _ in 0..8 {
        let count = Arc::clone(&count);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                *count.lock().unwrap() += 1;
            }
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    println!("total: {}", *count.lock().unwrap());
    println!("owners now: {}", Arc::strong_count(&count));
}
total: 8000
owners now: 1

The pattern is always the same: create Arc::new(Mutex::new(value)), and before spawning each thread, Arc::clone it and move the clone in. Every run prints 8000: no lost updates.

 main:     count --+
 thread 1: count --+--> Arc allocation [ strong: 9 | Mutex [ lock state | 0 ] ]
 ...               |
 thread 8: count --+

Read-only data needs no mutex at all: an Arc<Vec<String>> can be shared by many threads, since nobody can modify it.

Keep critical sections short

While one thread holds the lock, all others wanting it wait. Hold the guard only while you actually touch the data:

// bad: lock held during slow work
let mut guard = results.lock().unwrap();
let value = expensive(input);
guard.push(value);

// good: compute first, lock briefly
let value = expensive(input);
results.lock().unwrap().push(value);

A useful trick is to let the guard be a temporary in one statement (*count.lock().unwrap() += 1;) so it is dropped at the semicolon.

Deadlocks

A deadlock happens when threads wait for each other forever: thread 1 holds lock A and waits for B, thread 2 holds B and waits for A. Rust does not prevent deadlocks (they are not memory-unsafe). Avoid them by:

Locking the same Mutex twice in one thread also deadlocks (or panics): let a = m.lock(); let b = m.lock();.

RwLock: many readers or one writer

RwLock<T> allows any number of simultaneous readers (read()) or one writer (write()): the borrowing rule, enforced at run time across threads. It pays off for data that is read often and written rarely, like configuration.

Common mistakes

Recap

Going further: read the Rust Book chapter 16.3. Then replace the counter's Mutex<u64> with std::sync::atomic::AtomicU64 and fetch_add, covered briefly in the next lesson.

Exercises

Exercise 1 · write code

A shared counter

Read a number n from stdin. Spawn 8 threads that each increment a shared counter n times (one increment per lock), using Arc<Mutex<u64>>. Join them all and print:

final count: 8000

for input 1000.

File: main.rs

Starting point

use std::io;
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let mut s = String::new();
    io::stdin().read_line(&mut s).unwrap();
    let n: u64 = s.trim().parse().unwrap();
    let mut count = 0u64;
    let mut handles = Vec::new();
    for _ in 0..8 {
        handles.push(thread::spawn(|| {
            for _ in 0..n {
                count += 1;
            }
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    println!("final count: {count}");
}

Example

input

1000

expected output

final count: 8000

Example

input

0

expected output

final count: 0
Hint 1

The starter does not compile: eight threads cannot all mutably borrow one local. Wrap it: let count = Arc::new(Mutex::new(0u64));.

Hint 2

In the loop, give each thread its own Arc::clone(&count) and use a move closure.

Hint 3

*count.lock().unwrap() += 1; locks, increments, and unlocks at the end of the statement.

Reference solution
use std::io;
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let mut s = String::new();
    io::stdin().read_line(&mut s).unwrap();
    let n: u64 = s.trim().parse().unwrap();
    let count = Arc::new(Mutex::new(0u64));
    let mut handles = Vec::new();
    for _ in 0..8 {
        let count = Arc::clone(&count);
        handles.push(thread::spawn(move || {
            for _ in 0..n {
                *count.lock().unwrap() += 1;
            }
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    println!("final count: {}", *count.lock().unwrap());
}

Exercise 2 · write code

Threads filling a shared map

Read lines from stdin. Process them with 3 threads (thread t handles lines whose index i satisfies i % 3 == t); each thread counts words (lowercased, split on whitespace) into a shared Arc<Mutex<BTreeMap<String, usize>>>. After joining, print every word and its count in alphabetical order (word count). For

the cat
The dog
a cat

print

a 1
cat 2
dog 1
the 2

Share the lines with the threads through an Arc<Vec<String>>.

File: main.rs

Starting point

use std::collections::BTreeMap;
use std::io;
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let lines: Vec<String> = io::stdin().lines().map(|l| l.unwrap()).collect();
}

Example

input

the cat
The dog
a cat

expected output

a 1
cat 2
dog 1
the 2

Example

input

x
y
x
z
x

expected output

x 3
y 1
z 1
Hint 1

Wrap the lines in an Arc (read-only sharing needs no Mutex) and the map in Arc<Mutex<...>>.

Hint 2

Per thread: clone both Arcs, then move them into the closure along with t.

Hint 3

BTreeMap iterates in key order, so the output is deterministic no matter which thread counted what.

Reference solution
use std::collections::BTreeMap;
use std::io;
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let lines: Arc<Vec<String>> = Arc::new(io::stdin().lines().map(|l| l.unwrap()).collect());
    let counts: Arc<Mutex<BTreeMap<String, usize>>> = Arc::new(Mutex::new(BTreeMap::new()));
    let mut handles = Vec::new();
    for t in 0..3 {
        let lines = Arc::clone(&lines);
        let counts = Arc::clone(&counts);
        handles.push(thread::spawn(move || {
            for (i, line) in lines.iter().enumerate() {
                if i % 3 != t {
                    continue;
                }
                for w in line.split_whitespace() {
                    *counts.lock().unwrap().entry(w.to_lowercase()).or_insert(0) += 1;
                }
            }
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    for (w, c) in counts.lock().unwrap().iter() {
        println!("{w} {c}");
    }
}

Exercise 3 · quiz

When is the lock released?

let guard = data.lock().unwrap();
let n = guard.len();
heavy_computation(n);   // takes 2 seconds

What is the problem, and when is the mutex unlocked?

Answer
  • The guard lives until the end of the scope, so the mutex stays locked during heavy_computation, blocking all other threads

A MutexGuard unlocks when it is dropped, at the end of its scope (RAII, module 4). Holding it across slow work serializes every other thread behind it. Keep critical sections short: let n = data.lock().unwrap().len(); drops the guard at the end of that statement, or put the locked part in an inner block, or drop(guard) explicitly.