Skip to content
Learn Rust
Theme

Shared State: Arc and Mutex

Concurrency · Lesson 42

To share one value between threads you need two things. Arc gives the value several owners. Mutex makes sure only one thread changes it at a time.

Coming from GoA Rust Mutex holds the data inside it, instead of sitting next to it like sync.Mutex. lock() gives you a guard that unlocks when it goes out of scope, like a defer mu.Unlock() you cannot forget, and Arc lets several threads own the value because a Rust value has one owner, and a thread can outlive the scope that made it.

Coming from KotlinMutex replaces synchronized(lock), but the data lives inside the mutex, so you can't reach results without locking. Arc is a shared, reference-counted pointer that lets each thread own a handle to the same value.

Coming from Pythonresults.lock().unwrap() is your with lock:, and the lock is released when the guard goes out of scope. The data lives inside the Mutex, so forgetting to lock is a compile error, not a latent race that your tests may never show.

Coming from TypeScriptWorkers share only raw bytes in a SharedArrayBuffer; here Arc<Mutex<Vec<i32>>> shares an ordinary Vec between threads. lock() gives a guard that unlocks when it goes out of scope, and you can't touch the data without it.

Coming from JavaArc gives a value several owners, the way GC references share an object between threads, and Mutex<T> is synchronized with the data inside the lock. You can only reach the Vec by calling lock(), so forgetting to lock doesn't compile.

Coming from C#Mutex<T> is a lock that owns the data it protects, and Arc<T> shares it between threads where C# would rely on the garbage collector. The lock opens when you call lock() and closes at the end of the scope, and touching the data without it is a compile error.

Coming from CMutex<T> is a pthread_mutex_t stored together with the data, where the data is only reachable through lock(). Arc is an atomic reference count that frees the value when the last owner drops it; an unlocked write won't compile.

Coming from C++Arc<T> is std::shared_ptr<T>, and Mutex<T> is a std::mutex that wraps the data; lock() returns a guard like std::lock_guard. You can't reach the data without the guard, and sharing a type that isn't thread-safe, such as Rc, is a compile error.

The quick comparison

Rustsrc/main.rsRun ↗
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let results = Arc::new(Mutex::new(Vec::new()));
let mut handles = Vec::new();
for i in 0..3 {
let results = Arc::clone(&results); // another owner, same Vec
handles.push(thread::spawn(move || {
results.lock().unwrap().push(i * 10); // lock, push, unlock
}));
}
for h in handles {
h.join().unwrap();
}
let mut r = results.lock().unwrap();
r.sort();
println!("{:?}", *r);
}
$ cargo run
[0, 10, 20]
package main
import (
"fmt"
"sort"
"sync"
)
func main() {
var mu sync.Mutex
var results []int
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
mu.Lock()
results = append(results, i*10)
mu.Unlock()
}(i)
}
wg.Wait()
sort.Ints(results)
fmt.Println(results)
}
$ go run main.go
[0 10 20]
import kotlin.concurrent.thread
fun main() {
val lock = Any()
val results = mutableListOf<Int>()
val threads = (0 until 3).map { i ->
thread {
synchronized(lock) {
results.add(i * 10)
}
}
}
threads.forEach { it.join() }
results.sort()
println(results)
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
[0, 10, 20]
import threading
results: list[int] = []
lock = threading.Lock()
def work(i: int) -> None:
with lock: # lock, append, unlock
results.append(i * 10)
threads = [threading.Thread(target=work, args=(i,)) for i in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
results.sort()
print(results)
$ python3 main.py
[0, 10, 20]
import { Worker, isMainThread, workerData } from "node:worker_threads";
import { once } from "node:events";
// shared memory layout: [lock, length, item0, item1, item2]
const LOCK = 0;
const LEN = 1;
function lock(m: Int32Array): void {
while (Atomics.compareExchange(m, LOCK, 0, 1) !== 0) {
Atomics.wait(m, LOCK, 1); // sleep until the holder unlocks
}
}
function unlock(m: Int32Array): void {
Atomics.store(m, LOCK, 0);
Atomics.notify(m, LOCK, 1);
}
async function main(): Promise<void> {
// SharedArrayBuffer is the one thing workers really share
const shared = new Int32Array(new SharedArrayBuffer(4 * 5));
const workers = [0, 1, 2].map((i) => new Worker(__filename, { workerData: { shared, i } }));
await Promise.all(workers.map((w) => once(w, "exit")));
const results = Array.from(shared.subarray(2, 2 + shared[LEN])).sort((a, b) => a - b);
console.log(results);
}
if (isMainThread) {
main();
} else {
const { shared, i } = workerData as { shared: Int32Array; i: number };
lock(shared); // lock, push, unlock
shared[2 + shared[LEN]] = i * 10;
shared[LEN] += 1;
unlock(shared);
}
$ tsc --strict --module nodenext --types node main.ts && node main.js
[ 0, 10, 20 ]
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) throws InterruptedException {
List<Integer> results = new ArrayList<>();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 3; i++) {
int n = i;
threads.add(Thread.startVirtualThread(() -> {
synchronized (results) { // lock, add, unlock
results.add(n * 10);
}
}));
}
for (var t : threads) {
t.join();
}
results.sort(null);
System.out.println(results);
}
}
$ javac Main.java && java Main
[0, 10, 20]
var results = new List<int>();
var gate = new object();
var threads = new List<Thread>();
for (var i = 0; i < 3; i++)
{
var n = i;
var t = new Thread(() =>
{
lock (gate) // lock, add, unlock
{
results.Add(n * 10);
}
});
t.Start();
threads.Add(t);
}
foreach (var t in threads)
{
t.Join();
}
results.Sort();
Console.WriteLine($"[{string.Join(", ", results)}]");
$ dotnet run
[0, 10, 20]
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
// the list and its lock live together; every access takes the lock
struct Shared {
pthread_mutex_t lock;
int items[3];
int len;
};
struct Arg {
struct Shared *shared;
int i;
};
void *worker(void *p) {
struct Arg *a = p;
pthread_mutex_lock(&a->shared->lock); // lock, push, unlock
a->shared->items[a->shared->len++] = a->i * 10;
pthread_mutex_unlock(&a->shared->lock);
return NULL;
}
int cmp_int(const void *x, const void *y) {
return *(const int *)x - *(const int *)y;
}
int main(void) {
struct Shared results = {.lock = PTHREAD_MUTEX_INITIALIZER};
pthread_t threads[3];
struct Arg args[3];
for (int i = 0; i < 3; i++) {
args[i] = (struct Arg){&results, i};
pthread_create(&threads[i], NULL, worker, &args[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_mutex_lock(&results.lock);
qsort(results.items, results.len, sizeof(int), cmp_int);
printf("[%d, %d, %d]\n", results.items[0], results.items[1], results.items[2]);
pthread_mutex_unlock(&results.lock);
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
[0, 10, 20]
#include <algorithm>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
int main() {
std::vector<int> results;
std::mutex m;
{
std::vector<std::jthread> threads;
for (int i = 0; i < 3; ++i) {
threads.emplace_back([&results, &m, i] {
std::lock_guard lock(m); // lock, push, unlock at the closing brace
results.push_back(i * 10);
});
}
} // jthreads join here, so the references above never outlive results
std::ranges::sort(results);
std::cout << "[";
for (std::size_t i = 0; i < results.size(); ++i) {
std::cout << (i ? ", " : "") << results[i];
}
std::cout << "]\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
[0, 10, 20]

What's different

  • Arc::clone does not copy the Vec. It adds one to an atomic counter and gives you another pointer to the same Vec. When the last Arc goes away, the Vec is freed.
  • In many languages the mutex sits next to the data, and you have to remember to lock it. In Rust the Mutex holds the data. The only way to reach the Vec is lock(), so forgetting to lock does not compile.
  • The lock is released automatically when the value returned by lock() goes away, at the end of the line here. There is no Unlock to forget.
Gotcha

Several threads writing to one list, without a lock

Looks fine: in Python it runs fine; in TypeScript it runs but gives a wrong answer; in Go, Kotlin, Java, C#, C and C++ it runs but has a data race. Rust won't compile it.

Broken in Go. Rust catches it.Broken in Kotlin. Rust catches it.Fine in Python. Rust can't prove it's safe.Broken in TypeScript. Rust catches it.Broken in Java. Rust catches it.Broken in C#. Rust catches it.Broken in C. Rust catches it.Broken in C++. Rust catches it.
Rustsrc/main.rs✗ won't compile
use std::thread;
fn main() {
let mut results = Vec::new();
let mut handles = Vec::new();
for i in 0..3 {
handles.push(thread::spawn(move || {
results.push(i * 10);
}));
}
for h in handles {
h.join().unwrap();
}
println!("{results:?}");
}
package main
import (
"fmt"
"sync"
)
func main() {
var results []int
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
results = append(results, i*10) // no lock!
}(i)
}
wg.Wait()
fmt.Println(len(results))
}
$ go run -race main.go! runs · data race
==================
WARNING: DATA RACE
Read at 0x············ by goroutine 8:
  main.main.func1()
      main.go:15 +0x70
  main.main.gowrap1()
      main.go:16 +0x38

Previous write at 0x············ by goroutine 10:
  main.main.func1()
      main.go:15 +0xdc
  main.main.gowrap1()
      main.go:16 +0x38

Goroutine 8 (running) created at:
  main.main()
      main.go:13 +0x9c

Goroutine 10 (finished) created at:
  main.main()
      main.go:13 +0x9c
==================
==================
WARNING: DATA RACE
Read at 0x············ by goroutine 9:
  runtime.growslice()
      /opt/go/src/runtime/slice.go:178 +0x0
  main.main.func1()
      main.go:15 +0xa0
  main.main.gowrap1()
      main.go:16 +0x38

Previous write at 0x············ by goroutine 10:
  main.main.func1()
      main.go:15 +0xbc
  main.main.gowrap1()
      main.go:16 +0x38

Goroutine 9 (running) created at:
  main.main()
      main.go:13 +0x9c

Goroutine 10 (finished) created at:
  main.main()
      main.go:13 +0x9c
==================
2
Found 2 data race(s)

The output changes from run to run. This is one real run.

import kotlin.concurrent.thread
fun main() {
val results = ArrayList<Int>()
val threads = (0 until 3).map { i ->
thread { results.add(i * 10) } // no lock!
}
threads.forEach { it.join() }
println(results.size)
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar! runs · data race
3
import threading
results = []
def work(i):
results.append(i * 10) # no lock!
threads = [threading.Thread(target=work, args=(i,)) for i in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
print(sorted(results))
$ python3 main.py✓ runs
[0, 10, 20]
import { Worker, isMainThread, workerData } from "node:worker_threads";
import { once } from "node:events";
const results: number[] = [];
async function main(): Promise<void> {
const workers = [0, 1, 2].map((i) => new Worker(__filename, { workerData: i }));
await Promise.all(workers.map((w) => once(w, "exit")));
console.log(results);
}
if (isMainThread) {
main();
} else {
results.push(workerData * 10); // no lock!
}
$ tsc --strict --module nodenext --types node main.ts && node main.js! runs · wrong answer
[]
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) throws InterruptedException {
List<Integer> results = new ArrayList<>();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 4; i++) {
int n = i;
threads.add(new Thread(() -> {
for (int j = 0; j < 100_000; j++) {
results.add(n * 10); // no lock!
}
}));
}
threads.forEach(Thread::start);
for (var t : threads) {
t.join();
}
System.out.println(results.size() + " items (expected 400000)");
}
}
$ javac Main.java && java Main! runs · data race
102508 items (expected 400000)

The output changes from run to run. This is one real run.

var results = new List<int>(10_000_000); // room for everything up front
var start = new ManualResetEventSlim();
var threads = new List<Thread>();
for (var i = 0; i < 2; i++)
{
var t = new Thread(() =>
{
start.Wait(); // both begin together
for (var j = 0; j < 5_000_000; j++)
{
results.Add(j); // no lock!
}
});
t.Start();
threads.Add(t);
}
start.Set();
foreach (var t in threads)
{
t.Join();
}
Console.WriteLine($"expected 10000000, got {results.Count}");
$ dotnet run! runs · data race
expected 10000000, got 9125506

The output changes from run to run. This is one real run.

#include <pthread.h>
#include <stdio.h>
int results[3];
int len = 0;
void *worker(void *arg) {
int i = *(int *)arg;
results[len++] = i * 10; // no lock!
return NULL;
}
int main(void) {
pthread_t threads[3];
int ids[3];
for (int i = 0; i < 3; i++) {
ids[i] = i;
pthread_create(&threads[i], NULL, worker, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
printf("%d\n", len);
return 0;
}
$ gcc -fsanitize=thread -g main.c -o main && ./main! runs · data race
==================
WARNING: ThreadSanitizer: data race (pid=1)
  Read of size 4 at 0x············ by thread T2:
    #0 worker main.c:9 (main+0xaa8)

  Previous write of size 4 at 0x············ by thread T1:
    #0 worker main.c:9 (main+0xac4)

  Location is global 'len' of size 4 at 0x············ (main+0x2008c)

  Thread T2 (tid=1, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 main main.c:18 (main+0xbb0)

  Thread T1 (tid=1, finished) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 main main.c:18 (main+0xbb0)

SUMMARY: ThreadSanitizer: data race main.c:9 in worker
==================
==================
WARNING: ThreadSanitizer: data race (pid=1)
  Write of size 4 at 0x············ by thread T3:
    #0 worker main.c:9 (main+0xac4)

  Previous write of size 4 at 0x············ by thread T2:
    #0 worker main.c:9 (main+0xac4)

  Location is global 'len' of size 4 at 0x············ (main+0x2008c)

  Thread T3 (tid=1, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 main main.c:18 (main+0xbb0)

  Thread T2 (tid=1, finished) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 main main.c:18 (main+0xbb0)

SUMMARY: ThreadSanitizer: data race main.c:9 in worker
==================
3
ThreadSanitizer: reported 2 warnings

The output changes from run to run. This is one real run.

#include <iostream>
#include <thread>
#include <vector>
int main() {
std::vector<int> results;
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
threads.emplace_back([&results, i] {
results.push_back(i * 10); // no lock!
});
}
for (auto& t : threads) {
t.join();
}
std::cout << results.size() << "\n";
}
$ g++ -std=c++20 -fsanitize=thread -g main.cpp -o main && ./main! runs · data race
==================
WARNING: ThreadSanitizer: data race (pid=1)
  Read of size 8 at 0x············ by thread T2:
    #0 int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) /usr/include/c++/14/bits/vector.tcc:114 (main+0x2a90)
    #1 std::vector<int, std::allocator<int> >::push_back(int&&) /usr/include/c++/14/bits/stl_vector.h:1301 (main+0x2720)
    #2 operator() main.cpp:10 (main+0x13dc)
    #3 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (main+0x2088)
    #4 __invoke<main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (main+0x1fe4)
    #5 _M_invoke<0> /usr/include/c++/14/bits/std_thread.h:301 (main+0x1f24)
    #6 operator() /usr/include/c++/14/bits/std_thread.h:308 (main+0x1ec4)
    #7 _M_run /usr/include/c++/14/bits/std_thread.h:253 (main+0x1e70)
    #8 <null> <null> (libstdc++.so.6+0xdb4dc)

  Previous write of size 8 at 0x············ by thread T1:
    #0 void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&) /usr/include/c++/14/bits/vector.tcc:693 (main+0x3770)
    #1 int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) /usr/include/c++/14/bits/vector.tcc:123 (main+0x2b3c)
    #2 std::vector<int, std::allocator<int> >::push_back(int&&) /usr/include/c++/14/bits/stl_vector.h:1301 (main+0x2720)
    #3 operator() main.cpp:10 (main+0x13dc)
    #4 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (main+0x2088)
    #5 __invoke<main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (main+0x1fe4)
    #6 _M_invoke<0> /usr/include/c++/14/bits/std_thread.h:301 (main+0x1f24)
    #7 operator() /usr/include/c++/14/bits/std_thread.h:308 (main+0x1ec4)
    #8 _M_run /usr/include/c++/14/bits/std_thread.h:253 (main+0x1e70)
    #9 <null> <null> (libstdc++.so.6+0xdb4dc)

  Location is stack of main thread.

  Location is global '<null>' at 0x············ ([stack]+0x20988)

  Thread T2 (tid=1, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdb664)
    #2 construct_at<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/stl_construct.h:97 (main+0x1a64)
    #3 construct<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/alloc_traits.h:577 (main+0x190c)
    #4 _M_realloc_append<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:634 (main+0x190c)
    #5 emplace_back<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:123 (main+0x1654)
    #6 main main.cpp:9 (main+0x1480)

  Thread T1 (tid=1, finished) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdb664)
    #2 construct_at<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/stl_construct.h:97 (main+0x1a64)
    #3 construct<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/alloc_traits.h:577 (main+0x190c)
    #4 _M_realloc_append<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:634 (main+0x190c)
    #5 emplace_back<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:123 (main+0x1654)
    #6 main main.cpp:9 (main+0x1480)

SUMMARY: ThreadSanitizer: data race /usr/include/c++/14/bits/vector.tcc:114 in int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&)
==================
==================
WARNING: ThreadSanitizer: data race (pid=1)
  Read of size 8 at 0x············ by thread T3:
    #0 int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) /usr/include/c++/14/bits/vector.tcc:114 (main+0x2aa4)
    #1 std::vector<int, std::allocator<int> >::push_back(int&&) /usr/include/c++/14/bits/stl_vector.h:1301 (main+0x2720)
    #2 operator() main.cpp:10 (main+0x13dc)
    #3 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (main+0x2088)
    #4 __invoke<main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (main+0x1fe4)
    #5 _M_invoke<0> /usr/include/c++/14/bits/std_thread.h:301 (main+0x1f24)
    #6 operator() /usr/include/c++/14/bits/std_thread.h:308 (main+0x1ec4)
    #7 _M_run /usr/include/c++/14/bits/std_thread.h:253 (main+0x1e70)
    #8 <null> <null> (libstdc++.so.6+0xdb4dc)

  Previous write of size 8 at 0x············ by thread T1:
    #0 void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&) /usr/include/c++/14/bits/vector.tcc:694 (main+0x3798)
    #1 int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) /usr/include/c++/14/bits/vector.tcc:123 (main+0x2b3c)
    #2 std::vector<int, std::allocator<int> >::push_back(int&&) /usr/include/c++/14/bits/stl_vector.h:1301 (main+0x2720)
    #3 operator() main.cpp:10 (main+0x13dc)
    #4 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (main+0x2088)
    #5 __invoke<main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (main+0x1fe4)
    #6 _M_invoke<0> /usr/include/c++/14/bits/std_thread.h:301 (main+0x1f24)
    #7 operator() /usr/include/c++/14/bits/std_thread.h:308 (main+0x1ec4)
    #8 _M_run /usr/include/c++/14/bits/std_thread.h:253 (main+0x1e70)
    #9 <null> <null> (libstdc++.so.6+0xdb4dc)

  Location is stack of main thread.

  Location is global '<null>' at 0x············ ([stack]+0x20990)

  Thread T3 (tid=1, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdb664)
    #2 construct_at<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/stl_construct.h:97 (main+0x1a64)
    #3 construct<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/alloc_traits.h:577 (main+0x190c)
    #4 _M_realloc_append<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:634 (main+0x190c)
    #5 emplace_back<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:123 (main+0x1654)
    #6 main main.cpp:9 (main+0x1480)

  Thread T1 (tid=1, finished) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdb664)
    #2 construct_at<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/stl_construct.h:97 (main+0x1a64)
    #3 construct<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/alloc_traits.h:577 (main+0x190c)
    #4 _M_realloc_append<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:634 (main+0x190c)
    #5 emplace_back<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:123 (main+0x1654)
    #6 main main.cpp:9 (main+0x1480)

SUMMARY: ThreadSanitizer: data race /usr/include/c++/14/bits/vector.tcc:114 in int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&)
==================
==================
WARNING: ThreadSanitizer: data race (pid=1)
  Read of size 4 at 0x············ by thread T3:
    #0 memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115 (libtsan.so.2+0x89788)
    #1 memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:107 (libtsan.so.2+0x89788)
    #2 std::enable_if<std::__is_bitwise_relocatable<int, void>::value, int*>::type std::__relocate_a_1<int, int>(int*, int*, int*, std::allocator<int>&) /usr/include/c++/14/bits/stl_uninitialized.h:1129 (main+0x443c)
    #3 int* std::__relocate_a<int*, int*, std::allocator<int> >(int*, int*, int*, std::allocator<int>&) /usr/include/c++/14/bits/stl_uninitialized.h:1145 (main+0x4060)
    #4 std::vector<int, std::allocator<int> >::_S_relocate(int*, int*, int*, std::allocator<int>&) /usr/include/c++/14/bits/stl_vector.h:509 (main+0x3d84)
    #5 void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&) /usr/include/c++/14/bits/vector.tcc:647 (main+0x36f4)
    #6 int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) /usr/include/c++/14/bits/vector.tcc:123 (main+0x2b3c)
    #7 std::vector<int, std::allocator<int> >::push_back(int&&) /usr/include/c++/14/bits/stl_vector.h:1301 (main+0x2720)
    #8 operator() main.cpp:10 (main+0x13dc)
    #9 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (main+0x2088)
    #10 __invoke<main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (main+0x1fe4)
    #11 _M_invoke<0> /usr/include/c++/14/bits/std_thread.h:301 (main+0x1f24)
    #12 operator() /usr/include/c++/14/bits/std_thread.h:308 (main+0x1ec4)
    #13 _M_run /usr/include/c++/14/bits/std_thread.h:253 (main+0x1e70)
    #14 <null> <null> (libstdc++.so.6+0xdb4dc)

  Previous write of size 8 at 0x············ by thread T1:
    #0 operator new(unsigned long) ../../../../src/libsanitizer/tsan/tsan_new_delete.cpp:64 (libtsan.so.2+0xa2898)
    #1 std::__new_allocator<int>::allocate(unsigned long, void const*) /usr/include/c++/14/bits/new_allocator.h:151 (main+0x47a8)
    #2 std::allocator<int>::allocate(unsigned long) /usr/include/c++/14/bits/allocator.h:196 (main+0x3cf4)
    #3 std::allocator_traits<std::allocator<int> >::allocate(std::allocator<int>&, unsigned long) /usr/include/c++/14/bits/alloc_traits.h:515 (main+0x3cf4)
    #4 std::_Vector_base<int, std::allocator<int> >::_M_allocate(unsigned long) /usr/include/c++/14/bits/stl_vector.h:380 (main+0x3cf4)
    #5 void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&) /usr/include/c++/14/bits/vector.tcc:596 (main+0x366c)
    #6 int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) /usr/include/c++/14/bits/vector.tcc:123 (main+0x2b3c)
    #7 std::vector<int, std::allocator<int> >::push_back(int&&) /usr/include/c++/14/bits/stl_vector.h:1301 (main+0x2720)
    #8 operator() main.cpp:10 (main+0x13dc)
    #9 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (main+0x2088)
    #10 __invoke<main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (main+0x1fe4)
    #11 _M_invoke<0> /usr/include/c++/14/bits/std_thread.h:301 (main+0x1f24)
    #12 operator() /usr/include/c++/14/bits/std_thread.h:308 (main+0x1ec4)
    #13 _M_run /usr/include/c++/14/bits/std_thread.h:253 (main+0x1e70)
    #14 <null> <null> (libstdc++.so.6+0xdb4dc)

  Location is heap block of size 4 at 0x············ allocated by thread T1:
    #0 operator new(unsigned long) ../../../../src/libsanitizer/tsan/tsan_new_delete.cpp:64 (libtsan.so.2+0xa2898)
    #1 std::__new_allocator<int>::allocate(unsigned long, void const*) /usr/include/c++/14/bits/new_allocator.h:151 (main+0x47a8)
    #2 std::allocator<int>::allocate(unsigned long) /usr/include/c++/14/bits/allocator.h:196 (main+0x3cf4)
    #3 std::allocator_traits<std::allocator<int> >::allocate(std::allocator<int>&, unsigned long) /usr/include/c++/14/bits/alloc_traits.h:515 (main+0x3cf4)
    #4 std::_Vector_base<int, std::allocator<int> >::_M_allocate(unsigned long) /usr/include/c++/14/bits/stl_vector.h:380 (main+0x3cf4)
    #5 void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&) /usr/include/c++/14/bits/vector.tcc:596 (main+0x366c)
    #6 int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) /usr/include/c++/14/bits/vector.tcc:123 (main+0x2b3c)
    #7 std::vector<int, std::allocator<int> >::push_back(int&&) /usr/include/c++/14/bits/stl_vector.h:1301 (main+0x2720)
    #8 operator() main.cpp:10 (main+0x13dc)
    #9 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (main+0x2088)
    #10 __invoke<main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (main+0x1fe4)
    #11 _M_invoke<0> /usr/include/c++/14/bits/std_thread.h:301 (main+0x1f24)
    #12 operator() /usr/include/c++/14/bits/std_thread.h:308 (main+0x1ec4)
    #13 _M_run /usr/include/c++/14/bits/std_thread.h:253 (main+0x1e70)
    #14 <null> <null> (libstdc++.so.6+0xdb4dc)

  Thread T3 (tid=1, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdb664)
    #2 construct_at<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/stl_construct.h:97 (main+0x1a64)
    #3 construct<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/alloc_traits.h:577 (main+0x190c)
    #4 _M_realloc_append<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:634 (main+0x190c)
    #5 emplace_back<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:123 (main+0x1654)
    #6 main main.cpp:9 (main+0x1480)

  Thread T1 (tid=1, finished) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdb664)
    #2 construct_at<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/stl_construct.h:97 (main+0x1a64)
    #3 construct<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/alloc_traits.h:577 (main+0x190c)
    #4 _M_realloc_append<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:634 (main+0x190c)
    #5 emplace_back<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:123 (main+0x1654)
    #6 main main.cpp:9 (main+0x1480)

SUMMARY: ThreadSanitizer: data race ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115 in memcpy
==================
==================
WARNING: ThreadSanitizer: data race (pid=1)
  Read of size 8 at 0x············ by thread T2:
    #0 std::vector<int, std::allocator<int> >::size() const /usr/include/c++/14/bits/stl_vector.h:993 (main+0x2888)
    #1 std::vector<int, std::allocator<int> >::_M_check_len(unsigned long, char const*) const /usr/include/c++/14/bits/stl_vector.h:1908 (main+0x3208)
    #2 void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&) /usr/include/c++/14/bits/vector.tcc:590 (main+0x3600)
    #3 int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) /usr/include/c++/14/bits/vector.tcc:123 (main+0x2b3c)
    #4 std::vector<int, std::allocator<int> >::push_back(int&&) /usr/include/c++/14/bits/stl_vector.h:1301 (main+0x2720)
    #5 operator() main.cpp:10 (main+0x13dc)
    #6 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (main+0x2088)
    #7 __invoke<main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (main+0x1fe4)
    #8 _M_invoke<0> /usr/include/c++/14/bits/std_thread.h:301 (main+0x1f24)
    #9 operator() /usr/include/c++/14/bits/std_thread.h:308 (main+0x1ec4)
    #10 _M_run /usr/include/c++/14/bits/std_thread.h:253 (main+0x1e70)
    #11 <null> <null> (libstdc++.so.6+0xdb4dc)

  Previous write of size 8 at 0x············ by thread T1:
    #0 void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&) /usr/include/c++/14/bits/vector.tcc:692 (main+0x3758)
    #1 int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) /usr/include/c++/14/bits/vector.tcc:123 (main+0x2b3c)
    #2 std::vector<int, std::allocator<int> >::push_back(int&&) /usr/include/c++/14/bits/stl_vector.h:1301 (main+0x2720)
    #3 operator() main.cpp:10 (main+0x13dc)
    #4 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (main+0x2088)
    #5 __invoke<main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (main+0x1fe4)
    #6 _M_invoke<0> /usr/include/c++/14/bits/std_thread.h:301 (main+0x1f24)
    #7 operator() /usr/include/c++/14/bits/std_thread.h:308 (main+0x1ec4)
    #8 _M_run /usr/include/c++/14/bits/std_thread.h:253 (main+0x1e70)
    #9 <null> <null> (libstdc++.so.6+0xdb4dc)

  Location is stack of main thread.

  Location is global '<null>' at 0x············ ([stack]+0x20980)

  Thread T2 (tid=1, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdb664)
    #2 construct_at<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/stl_construct.h:97 (main+0x1a64)
    #3 construct<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/alloc_traits.h:577 (main+0x190c)
    #4 _M_realloc_append<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:634 (main+0x190c)
    #5 emplace_back<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:123 (main+0x1654)
    #6 main main.cpp:9 (main+0x1480)

  Thread T1 (tid=1, finished) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdb664)
    #2 construct_at<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/stl_construct.h:97 (main+0x1a64)
    #3 construct<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/alloc_traits.h:577 (main+0x190c)
    #4 _M_realloc_append<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:634 (main+0x190c)
    #5 emplace_back<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:123 (main+0x1654)
    #6 main main.cpp:9 (main+0x1480)

SUMMARY: ThreadSanitizer: data race /usr/include/c++/14/bits/stl_vector.h:993 in std::vector<int, std::allocator<int> >::size() const
==================
==================
WARNING: ThreadSanitizer: data race (pid=1)
  Write of size 8 at 0x············ by thread T2:
    #0 void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&) /usr/include/c++/14/bits/vector.tcc:692 (main+0x3758)
    #1 int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) /usr/include/c++/14/bits/vector.tcc:123 (main+0x2b3c)
    #2 std::vector<int, std::allocator<int> >::push_back(int&&) /usr/include/c++/14/bits/stl_vector.h:1301 (main+0x2720)
    #3 operator() main.cpp:10 (main+0x13dc)
    #4 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (main+0x2088)
    #5 __invoke<main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (main+0x1fe4)
    #6 _M_invoke<0> /usr/include/c++/14/bits/std_thread.h:301 (main+0x1f24)
    #7 operator() /usr/include/c++/14/bits/std_thread.h:308 (main+0x1ec4)
    #8 _M_run /usr/include/c++/14/bits/std_thread.h:253 (main+0x1e70)
    #9 <null> <null> (libstdc++.so.6+0xdb4dc)

  Previous write of size 8 at 0x············ by thread T3:
    #0 void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&) /usr/include/c++/14/bits/vector.tcc:692 (main+0x3758)
    #1 int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) /usr/include/c++/14/bits/vector.tcc:123 (main+0x2b3c)
    #2 std::vector<int, std::allocator<int> >::push_back(int&&) /usr/include/c++/14/bits/stl_vector.h:1301 (main+0x2720)
    #3 operator() main.cpp:10 (main+0x13dc)
    #4 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (main+0x2088)
    #5 __invoke<main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (main+0x1fe4)
    #6 _M_invoke<0> /usr/include/c++/14/bits/std_thread.h:301 (main+0x1f24)
    #7 operator() /usr/include/c++/14/bits/std_thread.h:308 (main+0x1ec4)
    #8 _M_run /usr/include/c++/14/bits/std_thread.h:253 (main+0x1e70)
    #9 <null> <null> (libstdc++.so.6+0xdb4dc)

  Location is stack of main thread.

  Location is global '<null>' at 0x············ ([stack]+0x20980)

  Thread T2 (tid=1, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdb664)
    #2 construct_at<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/stl_construct.h:97 (main+0x1a64)
    #3 construct<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/alloc_traits.h:577 (main+0x190c)
    #4 _M_realloc_append<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:634 (main+0x190c)
    #5 emplace_back<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:123 (main+0x1654)
    #6 main main.cpp:9 (main+0x1480)

  Thread T3 (tid=1, finished) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdb664)
    #2 construct_at<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/stl_construct.h:97 (main+0x1a64)
    #3 construct<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/alloc_traits.h:577 (main+0x190c)
    #4 _M_realloc_append<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:634 (main+0x190c)
    #5 emplace_back<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:123 (main+0x1654)
    #6 main main.cpp:9 (main+0x1480)

SUMMARY: ThreadSanitizer: data race /usr/include/c++/14/bits/vector.tcc:692 in void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&)
==================
==================
WARNING: ThreadSanitizer: data race (pid=1)
  Write of size 8 at 0x············ by thread T2:
    #0 void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&) /usr/include/c++/14/bits/vector.tcc:693 (main+0x3770)
    #1 int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) /usr/include/c++/14/bits/vector.tcc:123 (main+0x2b3c)
    #2 std::vector<int, std::allocator<int> >::push_back(int&&) /usr/include/c++/14/bits/stl_vector.h:1301 (main+0x2720)
    #3 operator() main.cpp:10 (main+0x13dc)
    #4 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (main+0x2088)
    #5 __invoke<main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (main+0x1fe4)
    #6 _M_invoke<0> /usr/include/c++/14/bits/std_thread.h:301 (main+0x1f24)
    #7 operator() /usr/include/c++/14/bits/std_thread.h:308 (main+0x1ec4)
    #8 _M_run /usr/include/c++/14/bits/std_thread.h:253 (main+0x1e70)
    #9 <null> <null> (libstdc++.so.6+0xdb4dc)

  Previous write of size 8 at 0x············ by thread T3:
    #0 void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&) /usr/include/c++/14/bits/vector.tcc:693 (main+0x3770)
    #1 int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) /usr/include/c++/14/bits/vector.tcc:123 (main+0x2b3c)
    #2 std::vector<int, std::allocator<int> >::push_back(int&&) /usr/include/c++/14/bits/stl_vector.h:1301 (main+0x2720)
    #3 operator() main.cpp:10 (main+0x13dc)
    #4 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (main+0x2088)
    #5 __invoke<main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (main+0x1fe4)
    #6 _M_invoke<0> /usr/include/c++/14/bits/std_thread.h:301 (main+0x1f24)
    #7 operator() /usr/include/c++/14/bits/std_thread.h:308 (main+0x1ec4)
    #8 _M_run /usr/include/c++/14/bits/std_thread.h:253 (main+0x1e70)
    #9 <null> <null> (libstdc++.so.6+0xdb4dc)

  Location is stack of main thread.

  Location is global '<null>' at 0x············ ([stack]+0x20988)

  Thread T2 (tid=1, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdb664)
    #2 construct_at<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/stl_construct.h:97 (main+0x1a64)
    #3 construct<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/alloc_traits.h:577 (main+0x190c)
    #4 _M_realloc_append<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:634 (main+0x190c)
    #5 emplace_back<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:123 (main+0x1654)
    #6 main main.cpp:9 (main+0x1480)

  Thread T3 (tid=1, finished) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdb664)
    #2 construct_at<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/stl_construct.h:97 (main+0x1a64)
    #3 construct<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/alloc_traits.h:577 (main+0x190c)
    #4 _M_realloc_append<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:634 (main+0x190c)
    #5 emplace_back<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:123 (main+0x1654)
    #6 main main.cpp:9 (main+0x1480)

SUMMARY: ThreadSanitizer: data race /usr/include/c++/14/bits/vector.tcc:693 in void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&)
==================
==================
WARNING: ThreadSanitizer: data race (pid=1)
  Write of size 8 at 0x············ by thread T2:
    #0 void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&) /usr/include/c++/14/bits/vector.tcc:694 (main+0x3798)
    #1 int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) /usr/include/c++/14/bits/vector.tcc:123 (main+0x2b3c)
    #2 std::vector<int, std::allocator<int> >::push_back(int&&) /usr/include/c++/14/bits/stl_vector.h:1301 (main+0x2720)
    #3 operator() main.cpp:10 (main+0x13dc)
    #4 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (main+0x2088)
    #5 __invoke<main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (main+0x1fe4)
    #6 _M_invoke<0> /usr/include/c++/14/bits/std_thread.h:301 (main+0x1f24)
    #7 operator() /usr/include/c++/14/bits/std_thread.h:308 (main+0x1ec4)
    #8 _M_run /usr/include/c++/14/bits/std_thread.h:253 (main+0x1e70)
    #9 <null> <null> (libstdc++.so.6+0xdb4dc)

  Previous write of size 8 at 0x············ by thread T3:
    #0 void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&) /usr/include/c++/14/bits/vector.tcc:694 (main+0x3798)
    #1 int& std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) /usr/include/c++/14/bits/vector.tcc:123 (main+0x2b3c)
    #2 std::vector<int, std::allocator<int> >::push_back(int&&) /usr/include/c++/14/bits/stl_vector.h:1301 (main+0x2720)
    #3 operator() main.cpp:10 (main+0x13dc)
    #4 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:61 (main+0x2088)
    #5 __invoke<main()::<lambda()> > /usr/include/c++/14/bits/invoke.h:96 (main+0x1fe4)
    #6 _M_invoke<0> /usr/include/c++/14/bits/std_thread.h:301 (main+0x1f24)
    #7 operator() /usr/include/c++/14/bits/std_thread.h:308 (main+0x1ec4)
    #8 _M_run /usr/include/c++/14/bits/std_thread.h:253 (main+0x1e70)
    #9 <null> <null> (libstdc++.so.6+0xdb4dc)

  Location is stack of main thread.

  Location is global '<null>' at 0x············ ([stack]+0x20990)

  Thread T2 (tid=1, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdb664)
    #2 construct_at<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/stl_construct.h:97 (main+0x1a64)
    #3 construct<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/alloc_traits.h:577 (main+0x190c)
    #4 _M_realloc_append<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:634 (main+0x190c)
    #5 emplace_back<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:123 (main+0x1654)
    #6 main main.cpp:9 (main+0x1480)

  Thread T3 (tid=1, finished) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5bbbc)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdb664)
    #2 construct_at<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/stl_construct.h:97 (main+0x1a64)
    #3 construct<std::thread, main()::<lambda()> > /usr/include/c++/14/bits/alloc_traits.h:577 (main+0x190c)
    #4 _M_realloc_append<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:634 (main+0x190c)
    #5 emplace_back<main()::<lambda()> > /usr/include/c++/14/bits/vector.tcc:123 (main+0x1654)
    #6 main main.cpp:9 (main+0x1480)

SUMMARY: ThreadSanitizer: data race /usr/include/c++/14/bits/vector.tcc:694 in void std::vector<int, std::allocator<int> >::_M_realloc_append<int>(int&&)
==================
3
ThreadSanitizer: reported 7 warnings

The output changes from run to run. This is one real run.

What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0382?
error[E0382]: borrow of moved value: `results`
  --> src/main.rs:14:16
   |
 4 |     let mut results = Vec::new();
   |         ----------- move occurs because `results` has type `Vec<i32>`, which does not implement the `Copy` trait
 5 |     let mut handles = Vec::new();
 6 |     for i in 0..3 {
   |     ------------- inside of this loop
 7 |         handles.push(thread::spawn(move || {
   |                                    ------- value moved into closure here, in previous iteration of loop
...
14 |     println!("{results:?}");
   |                ^^^^^^^ value borrowed here after move
   |
help: consider cloning the value before moving it into the closure
   |
 7 ~         let value = results.clone();
 8 ~         handles.push(thread::spawn(move || {
 9 ~             value.push(i * 10);
What happens in Go

With the race detector on, Go printed 2 here and reported a data race. Without a lock, two append calls can collide and lose items, so the count varies.

What happens in Kotlin

Kotlin printed 3 this time, but this is still a race. ArrayList isn't safe across threads (the Java docs say so), so another run can lose items.

What happens in Python

Python prints [0, 10, 20], because a single append is atomic in CPython. A read-then-write is not: the Python FAQ lists i = i + 1 as not atomic, and the Python section below shows which counter forms lose updates.

What happens in TypeScript

Prints [], an empty list. Each worker runs the file again and pushes into its own copy of results, so the main thread's list never changes.

What happens in Java

ArrayList is not thread-safe, so adds from different threads overwrite each other. The count comes out far below 400000 and changes every run.

What happens in C#

Two threads add to the same List with no lock, and the count comes up short of 10000000 (it changes every run). Without the capacity set up front, the List can also throw while it grows.

What happens in C

C compiles this without a word, and it usually prints 3. ThreadSanitizer shows the threads reading and writing len at the same time.

What happens in C++

This compiles cleanly. ThreadSanitizer flags the unlocked push_back calls, which a plain build does not.

What Rust sees

The first thread's move takes results. The second loop pass has nothing left to give. Sharing one Vec between three threads needs shared ownership and a lock, which is Arc<Mutex<_>> (or scoped threads that borrow a Mutex, shown below).

Why Rust says no

Code like this really is broken in most languages, and Rust refuses to build it. Safe Rust has no data races, because it checks before the program runs: a value shared between threads must be wrapped in something that controls access. What it does not check is the logic around the locks (see the sections on deadlocks and check-then-act). Careful with the compiler's help: here. It suggests cloning the Vec. Once you also make the copy mut, that compiles, but each thread would push into its own private copy, and results would stay empty. Arc and Mutex are the real fix.

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let results = Arc::new(Mutex::new(Vec::new()));
let mut handles = Vec::new();
for i in 0..3 {
let results = Arc::clone(&results);
handles.push(thread::spawn(move || {
results.lock().unwrap().push(i * 10);
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", results.lock().unwrap().len());
}
$ cargo run
3

The toolkit

Outputs come from Linux on 64-bit ARM (8 cores), Rust 1.99.1, debug build. Thread timing is not repeatable, so each Rust program here prints the same on every run: it joins its threads before printing, or uses a Barrier (threads wait at it until all have arrived) to force an order. Only the gotcha’s racy programs vary, and say so.

What Rust refuses

A data race is two accesses to the same memory with nothing ordering them, at least one a write and at least one not atomic (the wording of the atomic docs). In the gotcha near the top, move hands the list to the first thread (E0382). An Arc shares a value but, through Deref, hands out only &:

Rustsrc/main.rs✗ won't compile
use std::sync::Arc;
fn main() {
let list = Arc::new(vec![1, 2, 3]);
list.push(4);
println!("{list:?}");
}
$ cargo buildWhat is E0596?
error[E0596]: cannot borrow data in an `Arc` as mutable
 --> src/main.rs:5:5
  |
5 |     list.push(4);
  |     ^^^^ cannot borrow as mutable
  |
  = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Arc<Vec<i32>>`

A Mutex holds the value, so there is no way to reach it without asking:

Rustsrc/main.rs✗ won't compile
use std::sync::Mutex;
fn main() {
let list = Mutex::new(vec![1, 2, 3]);
list.push(4);
}
$ cargo buildWhat is E0599?
error[E0599]: no method named `push` found for struct `std::sync::Mutex<T>` in the current scope
 --> src/main.rs:5:10
  |
5 |     list.push(4);
  |          ^^^^ method not found in `std::sync::Mutex<Vec<{integer}>>`
  |
help: use `.lock().unwrap()` to borrow the `Vec<{integer}>`, blocking the current thread until it can be acquired
  |
5 |     list.lock().unwrap().push(4);
  |         ++++++++++++++++

  ... (notes pointing into the standard library trimmed)

The help: is the recipe: lock().unwrap(). Arc<Mutex<T>> means many owners (Arc), one user at a time (Mutex). In safe Rust a data race does not compile; deadlocks and logic races do.

Arc<T>: owners across threads

An Rc updates its count with no protection, so the compiler refuses to send one to a thread (Smart pointers shows the error). Arc is the same idea with an atomic count.

Rustsrc/main.rsRun ↗
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
println!("owners at the start: {}", Arc::strong_count(&data));
let copies: Vec<Arc<Vec<i32>>> = (0..3).map(|_| Arc::clone(&data)).collect();
println!("owners after three clones: {}", Arc::strong_count(&data));
println!("same Vec: {}", Arc::ptr_eq(&data, &copies[0]));
let handles: Vec<_> = copies
.into_iter()
.map(|mine| thread::spawn(move || mine.iter().sum::<i32>()))
.collect();
for h in handles {
println!("a thread summed: {}", h.join().unwrap());
}
println!("owners after the joins: {}", Arc::strong_count(&data));
}
$ cargo run
owners at the start: 1
owners after three clones: 4
same Vec: true
a thread summed: 6
a thread summed: 6
a thread summed: 6
owners after the joins: 1

Arc::clone copies a pointer and adds one to the count; the Vec is not copied (ptr_eq asks whether two Arcs share one allocation). Arc<RefCell<_>> is refused: Send means a value may move to another thread, Sync means a &T may be shared between threads, and RefCell is not Sync (Send and Sync shows the error). The fix is Mutex, the thread-safe cousin of RefCell.

Mutex<T>: the data lives inside the lock

lock() waits until no one else holds the lock, then returns a guard. The guard derefs to the data, and when it is dropped the lock opens (Common traits covers Drop). lock returns a Result because of poisoning, next section. It takes &self, so a shared &Mutex can still reach the data mutably: a Mutex is the thread-safe RefCell (Smart pointers). Each += 1 below is a read, an add and a write, and the lock makes those three steps one:

Rustsrc/main.rsRun ↗
use std::sync::{Arc, Mutex};
use std::thread;
fn count_with_threads(threads: usize, per_thread: usize) -> usize {
let total = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..threads)
.map(|_| {
let total = Arc::clone(&total);
thread::spawn(move || {
for _ in 0..per_thread {
*total.lock().unwrap() += 1;
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let n = *total.lock().unwrap();
n
}
fn main() {
println!("8 threads x 1000: {}", count_with_threads(8, 1000));
for round in 0..200 {
assert_eq!(count_with_threads(8, 1000), 8000, "round {round}");
}
println!("200 more rounds: every total was 8000");
}
$ cargo run
8 threads x 1000: 8000
200 more rounds: every total was 8000

try_lock returns Err at once instead of waiting, so the next programs use it to ask whether the lock is free:

Rustsrc/main.rsRun ↗
use std::sync::Mutex;
fn main() {
let m = Mutex::new(vec![1, 2, 3]);
{
let mut guard = m.lock().unwrap();
guard.push(4);
println!("guard alive: try_lock fails: {}", m.try_lock().is_err());
}
println!("block ended: try_lock works: {}", m.try_lock().is_ok());
m.lock().unwrap().push(5);
println!("statement ended: try_lock works: {}", m.try_lock().is_ok());
let guard = m.lock().unwrap();
println!("named guard: try_lock works: {}", m.try_lock().is_ok());
drop(guard);
println!(
"after drop(guard): try_lock works: {}",
m.try_lock().is_ok()
);
println!("data: {:?}", m.lock().unwrap());
}
$ cargo run
guard alive: try_lock fails: true
block ended: try_lock works: true
statement ended: try_lock works: true
named guard: try_lock works: false
after drop(guard): try_lock works: true
data: [1, 2, 3, 4, 5]

A temporary guard dies at the end of its statement; a named guard lives to the end of its block, or until drop. The surprise is a temporary in the head of a match, if let or while let, which lives through the whole body. An if condition releases it, and edition 2024 (this page’s) drops an if let guard before the else:

Rustsrc/main.rsRun ↗
use std::sync::Mutex;
fn free(m: &Mutex<Vec<i32>>) -> bool {
m.try_lock().is_ok()
}
fn main() {
let m = Mutex::new(vec![1, 2]);
if m.lock().unwrap().len() == 2 {
println!("in the if body: lock free is {}", free(&m));
}
match m.lock().unwrap().len() {
2 => println!("in the match arm: lock free is {}", free(&m)),
_ => {}
}
if let Some(first) = m.lock().unwrap().first().copied() {
println!(
"in the if let body (first is {first}): lock free is {}",
free(&m)
);
}
let n = m.lock().unwrap().len();
match n {
2 => println!("after `let n`: lock free is {}", free(&m)),
_ => {}
}
while let Some(x) = m.lock().unwrap().pop() {
println!("popped {x}: lock free is {}", free(&m));
}
if let Some(first) = m.lock().unwrap().first().copied() {
println!("unreachable: {first}");
} else {
println!(
"in the else branch (edition 2024): lock free is {}",
free(&m)
);
}
}
$ cargo run
in the if body: lock free is true
in the match arm: lock free is false
in the if let body (first is 1): lock free is false
after `let n`: lock free is true
popped 2: lock free is false
popped 1: lock free is false
in the else branch (edition 2024): lock free is true

Locking again in such a body, from the same thread, is a bug: the lock docs call the behaviour unspecified and say it might panic or deadlock. Bind the value first, as let n = ...; does. A Mutex is not reentrant because its guard gives &mut, and a second guard on one thread would be a second &mut to the same data; the unstable ReentrantLock (check-unstable-sync.fail.rs) hands out only & for that reason.

When a thread panics: poisoning

If a thread panics while it holds a guard, the data may be half updated. The Mutex records that, and the next lock() returns Err. The error still contains the guard:

Rustsrc/main.rsRun ↗
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let m = Arc::new(Mutex::new(vec![1, 2, 3]));
let m2 = Arc::clone(&m);
let result = thread::spawn(move || {
let mut guard = m2.lock().unwrap();
guard.push(4);
panic!("worker failed halfway");
})
.join();
println!("join returned an error: {}", result.is_err());
println!("is_poisoned: {}", m.is_poisoned());
match m.lock() {
Ok(_) => println!("lock: Ok"),
Err(e) => println!("lock: Err({e})"),
}
let guard = m.lock().unwrap_or_else(|e| e.into_inner());
println!("data after recovery: {:?}", *guard);
drop(guard);
println!("still poisoned: {}", m.is_poisoned());
m.clear_poison();
println!("after clear_poison: {}", m.is_poisoned());
}
$ cargo run
thread '<unnamed>' (1) panicked at src/main.rs:11:9:
worker failed halfway
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
join returned an error: true
is_poisoned: true
lock: Err(poisoned lock: another task failed inside)
data after recovery: [1, 2, 3, 4]
still poisoned: true
after clear_poison: false

The docs call the data “likely tainted”. into_inner on the error is the way back in, and clear_poison (Rust 1.77) marks the repair done. Most code simply unwraps, as the docs note, which turns one panic into a panic in every thread that touches the lock:

Rustsrc/main.rsRun ↗
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let m = Arc::new(Mutex::new(0));
let m2 = Arc::clone(&m);
let first = thread::spawn(move || {
let _guard = m2.lock().unwrap();
panic!("first thread failed");
})
.join();
println!("first thread failed: {}", first.is_err());
let m3 = Arc::clone(&m);
let second = thread::spawn(move || {
*m3.lock().unwrap() += 1;
})
.join();
println!("second thread failed too: {}", second.is_err());
}
$ cargo run
thread '<unnamed>' (1) panicked at src/main.rs:10:9:
first thread failed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
first thread failed: true

thread '<unnamed>' (1) panicked at src/main.rs:17:20:
called `Result::unwrap()` on an `Err` value: PoisonError { .. }
second thread failed too: true

That is a design choice. The translating section runs the same failure in Go, Java, Kotlin, C#, Python, C++ and C, where the lock simply opens and the next thread sees the half-done state. A non-poisoning Mutex is also unstable in Rust 1.99.1; the parking_lot crate’s Mutex does not poison.

RwLock<T>: many readers or one writer

read() gives a shared guard, any number at once. write() gives an exclusive one, and only a panicking writer poisons the lock. Two reader threads below meet at a barrier, so each must hold its guard while the other arrives; if readers excluded each other this would never finish:

Rustsrc/main.rsRun ↗
use std::sync::{Arc, Barrier, RwLock};
use std::thread;
fn main() {
let lock = Arc::new(RwLock::new(String::from("v1")));
// Two readers that both hold a read guard when they reach the barrier.
// If readers excluded each other, the second could never get in and this would not finish.
let barrier = Arc::new(Barrier::new(2));
let readers: Vec<_> = (0..2)
.map(|_| {
let (lock, barrier) = (Arc::clone(&lock), Arc::clone(&barrier));
thread::spawn(move || {
let guard = lock.read().unwrap();
barrier.wait();
guard.len()
})
})
.collect();
for r in readers {
println!("a reader saw {} bytes", r.join().unwrap());
}
let reader = lock.read().unwrap();
println!(
"reader alive, try_write fails: {}",
lock.try_write().is_err()
);
println!("reader alive, try_read works: {}", lock.try_read().is_ok());
drop(reader);
println!("reader gone, try_write works: {}", lock.try_write().is_ok());
lock.write().unwrap().push_str("+edit");
println!("value: {}", lock.read().unwrap());
}
$ cargo run
a reader saw 2 bytes
a reader saw 2 bytes
reader alive, try_write fails: true
reader alive, try_read works: true
reader gone, try_write works: true
value: v1+edit

If a writer is waiting, may new readers go ahead? The docs leave the policy to the operating system and promise none. An RwLock<T> is shared across threads only when T is Sync too, where a Mutex<T> needs just Send (check-rwlock-sync.fail.rs). Start with a Mutex, and switch when many threads really do read at once.

Atomics: one value, no lock

An atomic type changes a single integer, flag or pointer in one indivisible step; it exists for bool, the integers and raw pointers, not for a struct. fetch_add returns the old value. compare_exchange(current, new, ..) writes only if the value is still current, and says what it found otherwise. compare_exchange_weak may fail even when the values match, so it goes in a retry loop; try_update is that loop written for you (fetch_update is its older, now deprecated name). A static needs no Arc.

Rustsrc/main.rsRun ↗
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
static HITS: AtomicUsize = AtomicUsize::new(0);
fn main() {
let handles: Vec<_> = (0..8)
.map(|_| {
thread::spawn(|| {
for _ in 0..1000 {
HITS.fetch_add(1, Ordering::Relaxed);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
println!("hits: {}", HITS.load(Ordering::Relaxed));
let old = HITS.fetch_add(5, Ordering::SeqCst);
println!("fetch_add returned the old value: {old}");
println!(
"compare_exchange(8005, 0): {:?}",
HITS.compare_exchange(8005, 0, Ordering::SeqCst, Ordering::SeqCst)
);
println!(
"compare_exchange(8005, 1): {:?}",
HITS.compare_exchange(8005, 1, Ordering::SeqCst, Ordering::SeqCst)
);
// try_update retries its closure until the swap succeeds, or stops when the closure says None.
println!(
"try_update(add 1 below 5): {:?}",
HITS.try_update(Ordering::SeqCst, Ordering::SeqCst, |n| (n < 5)
.then_some(n + 1))
);
println!(
"fetch_max(9) returned the old value: {}",
HITS.fetch_max(9, Ordering::SeqCst)
);
}
$ cargo run
hits: 8000
fetch_add returned the old value: 8000
compare_exchange(8005, 0): Ok(8005)
compare_exchange(8005, 1): Err(0)
try_update(add 1 below 5): Ok(0)
fetch_max(9) returned the old value: 1

The extra arguments are orderings (Going deeper); use SeqCst when unsure. Atomics make each operation indivisible, not a sequence of them: this load-then-store loses an update every run, because the barrier makes both threads read first:

Rustsrc/main.rsRun ↗
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Barrier};
use std::thread;
fn main() {
let counter = Arc::new(AtomicUsize::new(0));
// The barrier makes both threads read before either writes, so the lost update happens every run.
let barrier = Arc::new(Barrier::new(2));
let handles: Vec<_> = (0..2)
.map(|_| {
let (counter, barrier) = (Arc::clone(&counter), Arc::clone(&barrier));
thread::spawn(move || {
let seen = counter.load(Ordering::SeqCst);
barrier.wait();
counter.store(seen + 1, Ordering::SeqCst);
})
})
.collect();
for h in handles {
h.join().unwrap();
}
println!(
"two increments, load then store: {}",
counter.load(Ordering::SeqCst)
);
counter.store(0, Ordering::SeqCst);
counter.fetch_add(1, Ordering::SeqCst);
counter.fetch_add(1, Ordering::SeqCst);
println!(
"two increments, fetch_add: {}",
counter.load(Ordering::SeqCst)
);
}
$ cargo run
two increments, load then store: 1
two increments, fetch_add: 2

Condvar: wait until something is true

A condition variable puts a thread to sleep until another thread says something changed. It pairs with a Mutex holding the state. wait_while releases the lock while asleep, takes it back on waking, and re-checks the condition, because a wake-up can be spurious (with no notify behind it; the docs say so). The closure says when to keep sleeping: the call “blocks the current thread until the provided condition becomes false”. The second half notifies before anyone waits, and the flag makes that safe.

Rustsrc/main.rsRun ↗
use std::sync::{Arc, Condvar, Mutex};
use std::thread;
fn main() {
let pair = Arc::new((Mutex::new(false), Condvar::new()));
let worker = {
let pair = Arc::clone(&pair);
thread::spawn(move || {
println!("worker: setting the flag");
let (flag, cvar) = &*pair;
*flag.lock().unwrap() = true;
cvar.notify_one();
})
};
let (flag, cvar) = &*pair;
// wait_while keeps sleeping while the closure returns true.
let guard = cvar
.wait_while(flag.lock().unwrap(), |ready| !*ready)
.unwrap();
println!("main: woke up, flag is {}", *guard);
drop(guard);
worker.join().unwrap();
// The notification can come before anyone waits. The flag is why that is fine.
let done = Arc::new((Mutex::new(false), Condvar::new()));
let d = Arc::clone(&done);
thread::spawn(move || {
*d.0.lock().unwrap() = true;
d.1.notify_one();
})
.join()
.unwrap();
let guard = done
.1
.wait_while(done.0.lock().unwrap(), |ready| !*ready)
.unwrap();
println!("late waiter: flag was already {}", *guard);
}
$ cargo run
worker: setting the flag
main: woke up, flag is true
late waiter: flag was already true

What Rust does not prevent

The type system rules out data races, and says nothing about these two problems.

Deadlock. Each thread below holds one lock and wants the other’s. try_lock shows what lock would have done, which is wait forever, so the program can run and report it:

Rustsrc/main.rsRun ↗
use std::sync::{Arc, Barrier, Mutex, TryLockError};
use std::thread;
type Account = Arc<Mutex<&'static str>>;
fn main() {
let a: Account = Arc::new(Mutex::new("account A"));
let b: Account = Arc::new(Mutex::new("account B"));
let barrier = Arc::new(Barrier::new(2));
// Each thread takes its first lock, waits until the other has too, then tries the second.
let spawn = |first: Account, second: Account| {
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
let _first = first.lock().unwrap();
barrier.wait();
let blocked = matches!(second.try_lock(), Err(TryLockError::WouldBlock));
barrier.wait(); // keep the first lock until the other thread has tried
blocked
})
};
let t1 = spawn(Arc::clone(&a), Arc::clone(&b));
let t2 = spawn(Arc::clone(&b), Arc::clone(&a));
println!(
"thread 1 (A, then B) would block on B: {}",
t1.join().unwrap()
);
println!(
"thread 2 (B, then A) would block on A: {}",
t2.join().unwrap()
);
}
$ cargo run
thread 1 (A, then B) would block on B: true
thread 2 (B, then A) would block on A: true

Nothing in a type records lock order. The cures are one agreed order (the second exercise), one lock at a time, or one lock for both.

Logic races. Send and Sync runs one: every access is locked, and the balance still goes negative, because the check and the change lock separately. The cure is one guard across both. Two threads each try to take 60 from 100, and exactly one succeeds in either order:

Rustsrc/main.rsRun ↗
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let balance = Arc::new(Mutex::new(100));
let handles: Vec<_> = (0..2)
.map(|_| {
let balance = Arc::clone(&balance);
thread::spawn(move || {
// One guard covers the check and the change.
let mut guard = balance.lock().unwrap();
if *guard >= 60 {
*guard -= 60;
true
} else {
false
}
})
})
.collect();
let taken = handles
.into_iter()
.map(|h| h.join().unwrap())
.filter(|&ok| ok)
.count();
println!("withdrawals that went through: {taken}");
println!("balance: {}", balance.lock().unwrap());
}
$ cargo run
withdrawals that went through: 1
balance: 40

Holding a guard too long

While a guard lives, every other thread that wants the lock waits. Do the slow work first and lock only to store the result:

Rustsrc/main.rsRun ↗
use std::sync::Mutex;
// Stands in for a slow call: a file read, a network request, a long calculation.
fn slow_part(results: &Mutex<Vec<u32>>) -> u32 {
println!(
" slow part runs; lock is held by someone: {}",
results.try_lock().is_err()
);
42
}
fn main() {
let results = Mutex::new(Vec::new());
println!("guard first, then the slow part:");
{
let mut guard = results.lock().unwrap();
guard.push(slow_part(&results));
}
println!("slow part first, then a short lock:");
let value = slow_part(&results);
results.lock().unwrap().push(value);
println!("results: {:?}", results.lock().unwrap());
}
$ cargo run
guard first, then the slow part:
  slow part runs; lock is held by someone: true
slow part first, then a short lock:
  slow part runs; lock is held by someone: false
results: [42, 42]

In async code a function can pause at an .await with its guard alive. A future holding a MutexGuard across an .await is not Send, so a runtime’s spawn refuses it (Async basics shows the error). A lock taken in a temporary, or in a block that ends before the .await, is accepted (check-guard-dropped.rs); a named guard that is used and then passed to drop is still refused on 1.99.1 (check-guard-drop.fail.rs). To hold a lock across an .await on purpose, Tokio has tokio::sync::Mutex.

Which tool, and how big a lock

You need Reach for Shown above
read-only data in many threads Arc<T> arc-count.rs
one value that many threads change Arc<Mutex<T>> counter.rs
many readers, occasional writer Arc<RwLock<T>> rwlock.rs
a counter, flag or id AtomicUsize, AtomicBool atomic.rs, release-acquire.rs
sleep until a condition holds Mutex plus Condvar condvar.rs
a value built once, first use wins OnceLock, LazyLock check-once.rs

If threads produce values that one place consumes, use a channel: a queue that moves each value from a sender to a receiver, so one thread owns it at any moment and no lock is needed. If many threads read and update one table, share it. Neither is the better one in general; the question is whether the data should move or stay put. The entry call does a map’s compound update inside the one lock, as Java’s merge does:

Rustsrc/main.rsRun ↗
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let hits: Arc<Mutex<HashMap<&str, u32>>> = Arc::new(Mutex::new(HashMap::new()));
let handles: Vec<_> = (0..4)
.map(|_| {
let hits = Arc::clone(&hits);
thread::spawn(move || {
for page in ["home", "docs", "home"] {
// One lock, and the update happens inside it: Java's map.merge(page, 1, Integer::sum).
*hits.lock().unwrap().entry(page).or_insert(0) += 1;
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let mut rows: Vec<_> = hits.lock().unwrap().iter().map(|(k, v)| (*k, *v)).collect();
rows.sort();
println!("hits: {rows:?}");
}
$ cargo run
hits: [("docs", 4), ("home", 8)]

std has no concurrent map: the options are a Mutex<HashMap>, a map split into shards, or a crate (dashmap for a map, see Maps; crossbeam-queue for queues). How much one lock covers is a separate choice. One lock around everything is the simplest and has no lock-order problem, but shuts out every other thread. A lock per item lets threads on different items run at once, though a view of several items needs several locks, in an agreed order. Shards sit in between: threads on different shards run together, and your code picks the shard (check-granularity.rs asserts these three).

Translating from what you know

Going deeper

Orderings, and what they promise

An ordering says what else becomes visible, not whether the operation is atomic: fetch_add is indivisible with any of them. By the docs, Relaxed means “no ordering constraints, only atomic operations”, and a pair of SeqCst operations also synchronizes other memory, in one total order. A flag announcing “the other data is ready” needs Release on the store and Acquire on the load:

Rustsrc/main.rsRun ↗
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::thread;
static DATA: AtomicUsize = AtomicUsize::new(0);
static READY: AtomicBool = AtomicBool::new(false);
fn main() {
thread::scope(|s| {
s.spawn(|| {
DATA.store(42, Ordering::Relaxed);
READY.store(true, Ordering::Release); // publishes what was written before it
});
s.spawn(|| {
while !READY.load(Ordering::Acquire) {
std::hint::spin_loop();
}
// This load saw the Release store, so the 42 is visible here.
println!("data after ready: {}", DATA.load(Ordering::Relaxed));
});
});
}
$ cargo run
data after ready: 42

Rust follows the C++20 model without consume. There is no default ordering: hits.fetch_add(1) is E0061 (check-atomic-default.fail.rs). compare_exchange takes one ordering for success and one for failure, and a Release failure ordering is a deny-by-default lint, because a failed exchange writes nothing (check-atomic-failure.fail.rs). When unsure, use SeqCst.

A Mutex is used the same way: what one thread wrote before unlocking is visible to the next thread that locks, as after leaving synchronized (Java’s rule). The Mutex docs do not use the words “happens-before”; the Nomicon calls Acquire and Release “perfectly suited for acquiring and releasing locks”.

What an Arc<Mutex<Vec>> is made of

let shared = Arc::new(Mutex::new(vec![1, 2, 3])); first and second are Arc::clone(&shared)
STACKHEAPshared: Arc<Mutex<Vec<i32>>>ptrfirst: Arc<Mutex<Vec<i32>>>ptrsecond: Arc<Mutex<Vec<i32>>>ptrone allocationstrong 3weak_count() 0lock and poison flagVec: ptr, len 3, cap 33 x i32123
Three owners, one lock, one Vec. The counts and sizes are those layout.rs printed: an Arc is 8 bytes, and the Mutex adds 8 bytes to the 24-byte Vec. What the 8 bytes hold, and the order of the cells, are not specified.
Rustsrc/main.rsRun ↗
use std::sync::{Arc, Mutex};
fn main() {
let shared = Arc::new(Mutex::new(vec![1, 2, 3]));
let first = Arc::clone(&shared);
let second = Arc::clone(&shared);
println!("strong_count: {}", Arc::strong_count(&shared));
println!("weak_count: {}", Arc::weak_count(&shared));
println!("same allocation: {}", Arc::ptr_eq(&first, &second));
let first_buffer = first.lock().unwrap().as_ptr();
let second_buffer = second.lock().unwrap().as_ptr();
println!("same buffer: {}", first_buffer == second_buffer);
let (len, capacity) = {
let guard = shared.lock().unwrap();
(guard.len(), guard.capacity())
};
println!("Vec length: {len}, capacity: {capacity}");
println!(
"size of an Arc: {} bytes",
size_of::<Arc<Mutex<Vec<i32>>>>()
);
println!("size of a Vec<i32>: {} bytes", size_of::<Vec<i32>>());
println!(
"size of a Mutex<Vec<i32>>: {} bytes",
size_of::<Mutex<Vec<i32>>>()
);
println!("size of a Mutex<()>: {} bytes", size_of::<Mutex<()>>());
}
$ cargo run
strong_count: 3
weak_count: 0
same allocation: true
same buffer: true
Vec length: 3, capacity: 3
size of an Arc: 8 bytes
size of a Vec<i32>: 24 bytes
size of a Mutex<Vec<i32>>: 32 bytes
size of a Mutex<()>: 8 bytes

Practice

Try it

Each exercise has checks in main: replace the todo!() calls and run until it prints all checks passed. The starters compile and stop at the first todo!(). Where a wrong answer could hang, the check fails with a message instead.

Compute outside the lock

get_or_compute caches slow results in a Mutex<HashMap>. The slow call runs with the lock released, and if another thread stored the key meanwhile, that value wins. It is not single-flight: two threads that miss together may both compute. Then write warm: an Arc around the table, a clone moved into each thread, a join for all. The checks fail a held guard, an overwrite, no caching, and copying the map.

Starterexercises/cache.rs
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
/// A cached number. It counts its clones, because a cache that copies entries (or the
/// whole map) to look one up has lost the point. Read the number out with `.0` instead.
pub struct Entry(u64);
static ENTRY_CLONES: AtomicUsize = AtomicUsize::new(0);
impl Clone for Entry {
fn clone(&self) -> Entry {
ENTRY_CLONES.fetch_add(1, Ordering::SeqCst);
Entry(self.0)
}
}
pub type Table = Mutex<HashMap<u32, Entry>>;
/// The cached value for `key`. On a miss, call `compute` and store its result.
/// `compute` may be slow, so no lock may be held while it runs. If another thread
/// stored a value for `key` while `compute` ran, keep that one and return it.
/// This is not "single-flight": two threads that miss together may both compute.
pub fn get_or_compute(table: &Table, key: u32, compute: impl FnOnce() -> u64) -> u64 {
todo!("look first; compute with the lock released; store with entry().or_insert")
}
/// Put a new table in an `Arc`. Start `threads` threads, each calling
/// `get_or_compute(&table, k, || compute(k))` for every key in `0..keys`. Join them
/// all, then return the table.
pub fn warm(keys: u32, threads: usize, compute: fn(u32) -> u64) -> Arc<Table> {
todo!(
"Arc::new, then Arc::clone for each thread, move it in, and join (add `use std::thread;`)"
)
}
static CALLS: AtomicUsize = AtomicUsize::new(0);
fn square(k: u32) -> u64 {
CALLS.fetch_add(1, Ordering::SeqCst);
u64::from(k) * u64::from(k)
}
fn main() {
// A miss computes once; a hit does not compute.
let table: Table = Mutex::new(HashMap::new());
let mut calls = 0;
assert_eq!(
get_or_compute(&table, 3, || {
calls += 1;
9
}),
9
);
assert_eq!(
get_or_compute(&table, 3, || panic!("a hit must not compute")),
9
);
assert_eq!(calls, 1, "computed once");
// The lock is free while `compute` runs.
let table: Table = Mutex::new(HashMap::new());
let value = get_or_compute(&table, 4, || {
assert!(
table.try_lock().is_ok(),
"compute ran while the lock was held"
);
16
});
assert_eq!(value, 16);
// Someone else stores key 7 while we compute: theirs wins, and we return theirs.
let table: Table = Mutex::new(HashMap::new());
let value = get_or_compute(&table, 7, || {
table.lock().unwrap().insert(7, Entry(999));
49
});
assert_eq!(value, 999, "keep the value that was stored first");
assert_eq!(get_or_compute(&table, 7, || panic!("already stored")), 999);
// `warm` shares one table between threads, and every thread has finished when it returns.
let table = warm(50, 4, square);
assert_eq!(
Arc::strong_count(&table),
1,
"join every thread before returning"
);
for k in 0..50 {
let hit = get_or_compute(&table, k, || panic!("warm did not store key {k}"));
assert_eq!(hit, u64::from(k) * u64::from(k));
}
let calls = CALLS.load(Ordering::SeqCst);
assert!(
(50..=200).contains(&calls),
"expected between 50 and 200 computations, got {calls}"
);
assert_eq!(
ENTRY_CLONES.load(Ordering::SeqCst),
0,
"an Entry was cloned: read the number out with `.0` instead of copying"
);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/cache.solution.rsRun ↗
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
/// A cached number. It counts its clones, because a cache that copies entries (or the
/// whole map) to look one up has lost the point. Read the number out with `.0` instead.
pub struct Entry(u64);
static ENTRY_CLONES: AtomicUsize = AtomicUsize::new(0);
impl Clone for Entry {
fn clone(&self) -> Entry {
ENTRY_CLONES.fetch_add(1, Ordering::SeqCst);
Entry(self.0)
}
}
pub type Table = Mutex<HashMap<u32, Entry>>;
/// The cached value for `key`. On a miss, call `compute` and store its result.
/// `compute` may be slow, so no lock may be held while it runs. If another thread
/// stored a value for `key` while `compute` ran, keep that one and return it.
/// This is not "single-flight": two threads that miss together may both compute.
pub fn get_or_compute(table: &Table, key: u32, compute: impl FnOnce() -> u64) -> u64 {
if let Some(entry) = table.lock().unwrap().get(&key) {
return entry.0;
}
let value = compute();
table.lock().unwrap().entry(key).or_insert(Entry(value)).0
}
use std::thread;
/// Put a new table in an `Arc`. Start `threads` threads, each calling
/// `get_or_compute(&table, k, || compute(k))` for every key in `0..keys`. Join them
/// all, then return the table.
pub fn warm(keys: u32, threads: usize, compute: fn(u32) -> u64) -> Arc<Table> {
let table = Arc::new(Mutex::new(HashMap::new()));
let handles: Vec<_> = (0..threads)
.map(|_| {
let table = Arc::clone(&table);
thread::spawn(move || {
for k in 0..keys {
get_or_compute(&table, k, || compute(k));
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
table
}
static CALLS: AtomicUsize = AtomicUsize::new(0);
fn square(k: u32) -> u64 {
CALLS.fetch_add(1, Ordering::SeqCst);
u64::from(k) * u64::from(k)
}
fn main() {
// A miss computes once; a hit does not compute.
let table: Table = Mutex::new(HashMap::new());
let mut calls = 0;
assert_eq!(
get_or_compute(&table, 3, || {
calls += 1;
9
}),
9
);
assert_eq!(
get_or_compute(&table, 3, || panic!("a hit must not compute")),
9
);
assert_eq!(calls, 1, "computed once");
// The lock is free while `compute` runs.
let table: Table = Mutex::new(HashMap::new());
let value = get_or_compute(&table, 4, || {
assert!(
table.try_lock().is_ok(),
"compute ran while the lock was held"
);
16
});
assert_eq!(value, 16);
// Someone else stores key 7 while we compute: theirs wins, and we return theirs.
let table: Table = Mutex::new(HashMap::new());
let value = get_or_compute(&table, 7, || {
table.lock().unwrap().insert(7, Entry(999));
49
});
assert_eq!(value, 999, "keep the value that was stored first");
assert_eq!(get_or_compute(&table, 7, || panic!("already stored")), 999);
// `warm` shares one table between threads, and every thread has finished when it returns.
let table = warm(50, 4, square);
assert_eq!(
Arc::strong_count(&table),
1,
"join every thread before returning"
);
for k in 0..50 {
let hit = get_or_compute(&table, k, || panic!("warm did not store key {k}"));
assert_eq!(hit, u64::from(k) * u64::from(k));
}
let calls = CALLS.load(Ordering::SeqCst);
assert!(
(50..=200).contains(&calls),
"expected between 50 and 200 computations, got {calls}"
);
assert_eq!(
ENTRY_CLONES.load(Ordering::SeqCst),
0,
"an Entry was cloned: read the number out with `.0` instead of copying"
);
println!("all checks passed");
}
$ cargo run
all checks passed

Lock in order, move money atomically

transfer needs two account locks, and opposite transfers deadlock if each takes from first. Lock the lower index first, handle from == to and an empty account, and call the provided lock so the checks can read your order. Hold both locks until both balances change: a reader sums every account while four threads transfer, and the total must be 3000 every time.

Starterexercises/bank.rs
use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, TryLockError};
use std::thread;
pub struct Bank {
accounts: Vec<Mutex<i64>>,
requests: Mutex<Vec<usize>>,
holding: AtomicUsize,
most_held: AtomicUsize,
careful: bool,
}
/// An account lock that counts how many are held at once. Use it like a `MutexGuard`.
pub struct Held<'a> {
guard: MutexGuard<'a, i64>,
bank: &'a Bank,
}
impl Deref for Held<'_> {
type Target = i64;
fn deref(&self) -> &i64 {
&self.guard
}
}
impl DerefMut for Held<'_> {
fn deref_mut(&mut self) -> &mut i64 {
&mut self.guard
}
}
impl Drop for Held<'_> {
fn drop(&mut self) {
self.bank.holding.fetch_sub(1, Ordering::SeqCst);
}
}
impl Bank {
pub fn new(balances: &[i64]) -> Bank {
Bank {
accounts: balances.iter().map(|&b| Mutex::new(b)).collect(),
requests: Mutex::new(Vec::new()),
holding: AtomicUsize::new(0),
most_held: AtomicUsize::new(0),
careful: false,
}
}
/// Like `new`, but `lock` panics instead of waiting when an account is already locked.
/// The single-threaded checks use it, so a self-deadlock is a message, not a hang.
pub fn careful(balances: &[i64]) -> Bank {
Bank {
careful: true,
..Bank::new(balances)
}
}
/// Lock account `i`. Always use this, so the checks can see your order and how many
/// locks you hold at the same time.
fn lock(&self, i: usize) -> Held<'_> {
self.requests.lock().unwrap().push(i);
let guard = if self.careful {
match self.accounts[i].try_lock() {
Ok(guard) => guard,
Err(TryLockError::WouldBlock) => {
panic!("account {i} is already locked: waiting for it would never end")
}
Err(TryLockError::Poisoned(_)) => panic!("account {i} is poisoned"),
}
} else {
self.accounts[i].lock().unwrap()
};
let now = self.holding.fetch_add(1, Ordering::SeqCst) + 1;
self.most_held.fetch_max(now, Ordering::SeqCst);
Held { guard, bank: self }
}
pub fn balance(&self, i: usize) -> i64 {
*self.accounts[i].lock().unwrap()
}
/// The sum of all balances, read while holding every account lock (in index order).
pub fn total(&self) -> i64 {
let all: Vec<_> = self.accounts.iter().map(|a| a.lock().unwrap()).collect();
all.iter().map(|g| **g).sum()
}
fn take_requests(&self) -> Vec<usize> {
std::mem::take(&mut *self.requests.lock().unwrap())
}
/// The most account locks held at the same time since the last call.
fn take_most_held(&self) -> usize {
self.most_held.swap(0, Ordering::SeqCst)
}
/// Move `amount` from account `from` to account `to`.
/// - Not enough money: `Err("insufficient funds")` and nothing changes.
/// - `from == to` is allowed and changes nothing; that account must not be locked twice.
/// - Two transfers in opposite directions must never wait for each other forever:
/// when two accounts are needed, lock the lower index first.
/// - Nobody may see the money gone from one account and not yet in the other:
/// hold both locks until both balances are changed.
pub fn transfer(&self, from: usize, to: usize, amount: i64) -> Result<(), String> {
todo!("lock in index order, then check the balance, then move the money")
}
}
fn main() {
// The lock order is the same whichever way the money goes, and both locks are held
// while the money moves.
let bank = Bank::careful(&[100, 50, 0]);
assert_eq!(bank.transfer(0, 1, 30), Ok(()));
assert_eq!(bank.take_requests(), [0, 1]);
assert_eq!(
bank.take_most_held(),
2,
"hold both locks while moving the money"
);
assert_eq!(bank.transfer(1, 0, 10), Ok(()));
assert_eq!(bank.take_requests(), [0, 1], "lock the lower index first");
assert_eq!(
bank.take_most_held(),
2,
"hold both locks while moving the money"
);
assert_eq!((bank.balance(0), bank.balance(1)), (80, 70));
// Not enough money: an error, and nothing moved.
assert_eq!(
bank.transfer(2, 0, 1),
Err("insufficient funds".to_string())
);
assert_eq!(
(bank.balance(0), bank.balance(1), bank.balance(2)),
(80, 70, 0)
);
bank.take_requests();
// The same account twice: no change, and the account is never locked twice.
assert_eq!(bank.transfer(1, 1, 5), Ok(()));
assert!(
bank.take_requests().len() <= 1,
"lock a single account at most once"
);
assert_eq!(bank.balance(1), 70);
// Opposite directions from many threads while a reader keeps adding up all the
// accounts. The total is 3000 every time, so nobody may see money in flight.
let bank = Arc::new(Bank::new(&[1000, 1000, 1000]));
let done = Arc::new(AtomicBool::new(false));
let reader = {
let (bank, done) = (Arc::clone(&bank), Arc::clone(&done));
thread::spawn(move || {
let mut sums = 0;
while !done.load(Ordering::SeqCst) {
assert_eq!(
bank.total(),
3000,
"money was missing in the middle of a transfer"
);
sums += 1;
}
sums
})
};
let handles: Vec<_> = (0..4)
.map(|t| {
let bank = Arc::clone(&bank);
thread::spawn(move || {
for i in 0..2000 {
let (from, to) = if (i + t) % 2 == 0 { (0, 2) } else { (2, 0) };
let _ = bank.transfer(from, to, 1);
let _ = bank.transfer(1, if i % 2 == 0 { 0 } else { 2 }, 1);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
done.store(true, Ordering::SeqCst);
assert!(reader.join().unwrap() > 0);
assert_eq!(bank.total(), 3000);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/bank.solution.rsRun ↗
use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, TryLockError};
use std::thread;
pub struct Bank {
accounts: Vec<Mutex<i64>>,
requests: Mutex<Vec<usize>>,
holding: AtomicUsize,
most_held: AtomicUsize,
careful: bool,
}
/// An account lock that counts how many are held at once. Use it like a `MutexGuard`.
pub struct Held<'a> {
guard: MutexGuard<'a, i64>,
bank: &'a Bank,
}
impl Deref for Held<'_> {
type Target = i64;
fn deref(&self) -> &i64 {
&self.guard
}
}
impl DerefMut for Held<'_> {
fn deref_mut(&mut self) -> &mut i64 {
&mut self.guard
}
}
impl Drop for Held<'_> {
fn drop(&mut self) {
self.bank.holding.fetch_sub(1, Ordering::SeqCst);
}
}
impl Bank {
pub fn new(balances: &[i64]) -> Bank {
Bank {
accounts: balances.iter().map(|&b| Mutex::new(b)).collect(),
requests: Mutex::new(Vec::new()),
holding: AtomicUsize::new(0),
most_held: AtomicUsize::new(0),
careful: false,
}
}
/// Like `new`, but `lock` panics instead of waiting when an account is already locked.
/// The single-threaded checks use it, so a self-deadlock is a message, not a hang.
pub fn careful(balances: &[i64]) -> Bank {
Bank {
careful: true,
..Bank::new(balances)
}
}
/// Lock account `i`. Always use this, so the checks can see your order and how many
/// locks you hold at the same time.
fn lock(&self, i: usize) -> Held<'_> {
self.requests.lock().unwrap().push(i);
let guard = if self.careful {
match self.accounts[i].try_lock() {
Ok(guard) => guard,
Err(TryLockError::WouldBlock) => {
panic!("account {i} is already locked: waiting for it would never end")
}
Err(TryLockError::Poisoned(_)) => panic!("account {i} is poisoned"),
}
} else {
self.accounts[i].lock().unwrap()
};
let now = self.holding.fetch_add(1, Ordering::SeqCst) + 1;
self.most_held.fetch_max(now, Ordering::SeqCst);
Held { guard, bank: self }
}
pub fn balance(&self, i: usize) -> i64 {
*self.accounts[i].lock().unwrap()
}
/// The sum of all balances, read while holding every account lock (in index order).
pub fn total(&self) -> i64 {
let all: Vec<_> = self.accounts.iter().map(|a| a.lock().unwrap()).collect();
all.iter().map(|g| **g).sum()
}
fn take_requests(&self) -> Vec<usize> {
std::mem::take(&mut *self.requests.lock().unwrap())
}
/// The most account locks held at the same time since the last call.
fn take_most_held(&self) -> usize {
self.most_held.swap(0, Ordering::SeqCst)
}
/// Move `amount` from account `from` to account `to`.
/// - Not enough money: `Err("insufficient funds")` and nothing changes.
/// - `from == to` is allowed and changes nothing; that account must not be locked twice.
/// - Two transfers in opposite directions must never wait for each other forever:
/// when two accounts are needed, lock the lower index first.
/// - Nobody may see the money gone from one account and not yet in the other:
/// hold both locks until both balances are changed.
pub fn transfer(&self, from: usize, to: usize, amount: i64) -> Result<(), String> {
if from == to {
return Ok(());
}
let (low, high) = if from < to { (from, to) } else { (to, from) };
let mut low_guard = self.lock(low);
let mut high_guard = self.lock(high);
let (source, target) = if from < to {
(&mut *low_guard, &mut *high_guard)
} else {
(&mut *high_guard, &mut *low_guard)
};
if *source < amount {
return Err("insufficient funds".to_string());
}
*source -= amount;
*target += amount;
Ok(())
}
}
fn main() {
// The lock order is the same whichever way the money goes, and both locks are held
// while the money moves.
let bank = Bank::careful(&[100, 50, 0]);
assert_eq!(bank.transfer(0, 1, 30), Ok(()));
assert_eq!(bank.take_requests(), [0, 1]);
assert_eq!(
bank.take_most_held(),
2,
"hold both locks while moving the money"
);
assert_eq!(bank.transfer(1, 0, 10), Ok(()));
assert_eq!(bank.take_requests(), [0, 1], "lock the lower index first");
assert_eq!(
bank.take_most_held(),
2,
"hold both locks while moving the money"
);
assert_eq!((bank.balance(0), bank.balance(1)), (80, 70));
// Not enough money: an error, and nothing moved.
assert_eq!(
bank.transfer(2, 0, 1),
Err("insufficient funds".to_string())
);
assert_eq!(
(bank.balance(0), bank.balance(1), bank.balance(2)),
(80, 70, 0)
);
bank.take_requests();
// The same account twice: no change, and the account is never locked twice.
assert_eq!(bank.transfer(1, 1, 5), Ok(()));
assert!(
bank.take_requests().len() <= 1,
"lock a single account at most once"
);
assert_eq!(bank.balance(1), 70);
// Opposite directions from many threads while a reader keeps adding up all the
// accounts. The total is 3000 every time, so nobody may see money in flight.
let bank = Arc::new(Bank::new(&[1000, 1000, 1000]));
let done = Arc::new(AtomicBool::new(false));
let reader = {
let (bank, done) = (Arc::clone(&bank), Arc::clone(&done));
thread::spawn(move || {
let mut sums = 0;
while !done.load(Ordering::SeqCst) {
assert_eq!(
bank.total(),
3000,
"money was missing in the middle of a transfer"
);
sums += 1;
}
sums
})
};
let handles: Vec<_> = (0..4)
.map(|t| {
let bank = Arc::clone(&bank);
thread::spawn(move || {
for i in 0..2000 {
let (from, to) = if (i + t) % 2 == 0 { (0, 2) } else { (2, 0) };
let _ = bank.transfer(from, to, 1);
let _ = bank.transfer(1, if i % 2 == 0 { 0 } else { 2 }, 1);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
done.store(true, Ordering::SeqCst);
assert!(reader.join().unwrap() > 0);
assert_eq!(bank.total(), 3000);
println!("all checks passed");
}
$ cargo run
all checks passed

A retry loop without a lock

Write update, your own try_update, on a Slot with only load, store and compare_exchange. A check plays a rival thread that changes the slot while your closure runs, which fails a load-then-store and a single compare_exchange. Then write claim_all: one slot shared with Arc, and per-thread counts that add up to the limit exactly.

Starterexercises/update.rs
use std::cell::Cell;
use std::sync::atomic::{AtomicUsize, Ordering};
mod slot {
use super::*;
thread_local! {
static FAILED_WITH: Cell<Option<usize>> = const { Cell::new(None) };
}
/// A number that threads share. The atomic inside is private to this module, so the
/// only ways in are the three methods below.
pub struct Slot(AtomicUsize);
impl Slot {
pub fn new(value: usize) -> Slot {
Slot(AtomicUsize::new(value))
}
pub fn load(&self) -> usize {
FAILED_WITH.with(|failed| failed.set(None));
self.0.load(Ordering::SeqCst)
}
/// A plain store. Provided so the checks can play the part of a rival thread.
pub fn store(&self, value: usize) {
self.0.store(value, Ordering::SeqCst);
}
/// Store `new` only if the slot still holds `current`. Returns `Ok(current)`, or
/// `Err(value)` with the value that was there instead. Asking again with the
/// `current` that just failed, without a `load` in between, would fail forever,
/// so that panics.
pub fn compare_exchange(&self, current: usize, new: usize) -> Result<usize, usize> {
FAILED_WITH.with(|failed| {
assert_ne!(
failed.get(),
Some(current),
"retry with the value compare_exchange gave back, not the one that just failed"
);
let result =
self.0
.compare_exchange(current, new, Ordering::SeqCst, Ordering::SeqCst);
failed.set(result.is_err().then_some(current));
result
})
}
}
}
use slot::Slot;
/// Update the slot with `f`, without a lock. Read the value, ask `f` for a replacement,
/// and store it with `compare_exchange`. If another thread changed the slot in between,
/// do not store: ask `f` again with the value that is there now.
/// - `f` returns `None`: stop and return `Err(value it was given)`.
/// - A swap succeeds: return `Ok(value it replaced)`.
pub fn update(slot: &Slot, f: impl FnMut(usize) -> Option<usize>) -> Result<usize, usize> {
todo!(
"loop: load, call f (make it `mut f`), compare_exchange; on Err take the value it found and go round again"
)
}
/// Start `threads` threads that share one slot (starting at 0) and each call `update`
/// to add 1 while the slot is below `limit`, until `update` says no. Join them all and
/// return how many increments each thread made.
pub fn claim_all(limit: usize, threads: usize) -> Vec<usize> {
todo!(
"Arc::new, Arc::clone for each thread, move it in, and collect what join returns (add `use std::sync::Arc;` and `use std::thread;`)"
)
}
fn main() {
// A closure that says yes: the old value comes back and the slot changes.
let slot = Slot::new(5);
assert_eq!(update(&slot, |n| Some(n + 1)), Ok(5));
assert_eq!(slot.load(), 6);
// A closure that says no: the current value comes back and nothing changes.
assert_eq!(update(&slot, |_| None), Err(6));
assert_eq!(slot.load(), 6);
// A rival changes the slot while `f` is thinking. The first swap must fail, and
// `f` must be asked again with the rival's value.
let slot = Slot::new(5);
let mut seen = Vec::new();
let result = update(&slot, |n| {
seen.push(n);
if seen.len() == 1 {
slot.store(100);
}
Some(n + 1)
});
assert_eq!(
seen,
[5, 100],
"ask f again with the value that is there now"
);
assert_eq!(result, Ok(100));
assert_eq!(
slot.load(),
101,
"the rival's value must not be overwritten"
);
// The rival's value can also make f say no.
let slot = Slot::new(1);
let result = update(&slot, |n| {
if n == 1 {
slot.store(50);
}
(n < 10).then_some(n + 1)
});
assert_eq!(result, Err(50));
assert_eq!(slot.load(), 50);
// Threads race to take slots 1..=limit. Every slot is taken once, none twice.
for (limit, threads) in [(0, 4), (1, 4), (1000, 8), (5000, 8)] {
let counts = claim_all(limit, threads);
assert_eq!(counts.len(), threads, "one count per thread");
assert_eq!(counts.iter().sum::<usize>(), limit, "limit {limit}");
}
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/update.solution.rsRun ↗
use std::cell::Cell;
use std::sync::atomic::{AtomicUsize, Ordering};
mod slot {
use super::*;
thread_local! {
static FAILED_WITH: Cell<Option<usize>> = const { Cell::new(None) };
}
/// A number that threads share. The atomic inside is private to this module, so the
/// only ways in are the three methods below.
pub struct Slot(AtomicUsize);
impl Slot {
pub fn new(value: usize) -> Slot {
Slot(AtomicUsize::new(value))
}
pub fn load(&self) -> usize {
FAILED_WITH.with(|failed| failed.set(None));
self.0.load(Ordering::SeqCst)
}
/// A plain store. Provided so the checks can play the part of a rival thread.
pub fn store(&self, value: usize) {
self.0.store(value, Ordering::SeqCst);
}
/// Store `new` only if the slot still holds `current`. Returns `Ok(current)`, or
/// `Err(value)` with the value that was there instead. Asking again with the
/// `current` that just failed, without a `load` in between, would fail forever,
/// so that panics.
pub fn compare_exchange(&self, current: usize, new: usize) -> Result<usize, usize> {
FAILED_WITH.with(|failed| {
assert_ne!(
failed.get(),
Some(current),
"retry with the value compare_exchange gave back, not the one that just failed"
);
let result =
self.0
.compare_exchange(current, new, Ordering::SeqCst, Ordering::SeqCst);
failed.set(result.is_err().then_some(current));
result
})
}
}
}
use slot::Slot;
/// Update the slot with `f`, without a lock. Read the value, ask `f` for a replacement,
/// and store it with `compare_exchange`. If another thread changed the slot in between,
/// do not store: ask `f` again with the value that is there now.
/// - `f` returns `None`: stop and return `Err(value it was given)`.
/// - A swap succeeds: return `Ok(value it replaced)`.
pub fn update(slot: &Slot, mut f: impl FnMut(usize) -> Option<usize>) -> Result<usize, usize> {
let mut current = slot.load();
loop {
let Some(new) = f(current) else {
return Err(current);
};
match slot.compare_exchange(current, new) {
Ok(previous) => return Ok(previous),
Err(found) => current = found,
}
}
}
use std::sync::Arc;
use std::thread;
/// Start `threads` threads that share one slot (starting at 0) and each call `update`
/// to add 1 while the slot is below `limit`, until `update` says no. Join them all and
/// return how many increments each thread made.
pub fn claim_all(limit: usize, threads: usize) -> Vec<usize> {
let slot = Arc::new(Slot::new(0));
let handles: Vec<_> = (0..threads)
.map(|_| {
let slot = Arc::clone(&slot);
thread::spawn(move || {
let mut mine = 0;
while update(&slot, |n| (n < limit).then_some(n + 1)).is_ok() {
mine += 1;
}
mine
})
})
.collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
}
fn main() {
// A closure that says yes: the old value comes back and the slot changes.
let slot = Slot::new(5);
assert_eq!(update(&slot, |n| Some(n + 1)), Ok(5));
assert_eq!(slot.load(), 6);
// A closure that says no: the current value comes back and nothing changes.
assert_eq!(update(&slot, |_| None), Err(6));
assert_eq!(slot.load(), 6);
// A rival changes the slot while `f` is thinking. The first swap must fail, and
// `f` must be asked again with the rival's value.
let slot = Slot::new(5);
let mut seen = Vec::new();
let result = update(&slot, |n| {
seen.push(n);
if seen.len() == 1 {
slot.store(100);
}
Some(n + 1)
});
assert_eq!(
seen,
[5, 100],
"ask f again with the value that is there now"
);
assert_eq!(result, Ok(100));
assert_eq!(
slot.load(),
101,
"the rival's value must not be overwritten"
);
// The rival's value can also make f say no.
let slot = Slot::new(1);
let result = update(&slot, |n| {
if n == 1 {
slot.store(50);
}
(n < 10).then_some(n + 1)
});
assert_eq!(result, Err(50));
assert_eq!(slot.load(), 50);
// Threads race to take slots 1..=limit. Every slot is taken once, none twice.
for (limit, threads) in [(0, 4), (1, 4), (1000, 8), (5000, 8)] {
let counts = claim_all(limit, threads);
assert_eq!(counts.len(), threads, "one count per thread");
assert_eq!(counts.iter().sum::<usize>(), limit, "limit {limit}");
}
println!("all checks passed");
}
$ cargo run
all checks passed

Read more