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
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);}[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)}[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)}[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)[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);}[ 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); }}[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)}]");[0, 10, 20]
#include <pthread.h>#include <stdio.h>#include <stdlib.h>
// the list and its lock live together; every access takes the lockstruct 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;}[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";}[0, 10, 20]
What's different
Arc::clonedoes 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 noUnlockto forget.
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.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))}==================
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)}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))[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!}[]
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)"); }}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 frontvar 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}");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;}==================
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 warningsThe 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";}==================
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 warningsThe output changes from run to run. This is one real run.
What does the compiler say? Guess first, then open.
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);
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.
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.
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.
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.
ArrayList is not thread-safe, so adds from different threads overwrite each other. The count comes out far below 400000 and changes every run.
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.
C compiles this without a word, and it usually prints 3. ThreadSanitizer shows the threads reading and writing len at the same time.
This compiles cleanly. ThreadSanitizer flags the unlocked push_back calls, which a plain build does not.
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).
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.
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());}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 &:
use std::sync::Arc;
fn main() { let list = Arc::new(vec![1, 2, 3]); list.push(4); println!("{list:?}");}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:
use std::sync::Mutex;
fn main() { let list = Mutex::new(vec![1, 2, 3]); list.push(4);}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.
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));}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:
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");}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:
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());}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:
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) ); }}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:
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());}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:
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());}
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: trueThat 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:
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());}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.
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) );}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:
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) );}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.
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);}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:
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() );}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:
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());}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:
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());}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:
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:?}");}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:
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)); }); });}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
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<()>>());}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.
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
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");}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.
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
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");}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.
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
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");}all checks passed
Read more
- The Rust Book, chapter 16.3: Shared-State Concurrency, and Rust by Example: Arc.
- The standard library:
Mutex,RwLock,Arc,Condvar,OnceLock,atomicandOrdering, and thestd::syncoverview. - The Rustonomicon on data races (what safe Rust guarantees) and atomics (what the orderings guarantee).
- Errors: E0382, E0596, E0599, E0061, or this site’s index.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.