mutex.md (7536B)
1 # Mutex 2 3 Mutex is the most commonly used tool for sharing (mutable) data between threads. 4 Mutex is short for "Mutural Exclusion". It uses UnsafeCell under the hood, and 5 provides a safe interface MutexGuard for threads to access the data with 6 "Interior Mutability". It is a thread-safe wrapper around some data T that 7 ensures only one thread can access that data at a time 8 9 ``` 10 +-----------------------------+ 11 | Arc<Mutex<T>> | 12 | (shared across threads) | 13 | | 14 | +---------------------+ | 15 | | Mutex<T> | | 16 | | (lock + the data) | | 17 | | | | 18 | | +-------------+ | | 19 | | | value T | | | 20 | | +-------------+ | | 21 | +----------^----------+ | 22 +--------------|--------------+ 23 | 24 clones of Arc | 25 (Arc::clone(&mutex)) | 26 sent to threads | 27 | 28 +------------------+------------------+ 29 | | | 30 v v v 31 +----------------+ +----------------+ +----------------+ 32 | thread 1 | | thread 2 | | thread 3 | 33 | | | | | | 34 | lock() | | lock() | | lock() | 35 | | | | | | | | | 36 | v | | v | | v | 37 | MutexGuard<T> | | MutexGuard<T> | | MutexGuard<T> | 38 | (exclusive | | (exclusive | | (exclusive | 39 | access) | | access) | | access) | 40 +----------------+ +----------------+ +----------------+ 41 42 Only ONE MutexGuard can exist at a time: 43 - When a thread calls lock(), it blocks until it gets a MutexGuard. 44 - While a thread holds the guard, others wait. 45 - When the guard is dropped (goes out of scope), the lock is released. 46 ``` 47 48 ## Basic Example: 49 50 ```rust 51 use std::sync::Mutex; 52 53 fn main() { 54 let my_mutex = Mutex::new(5); 55 56 { 57 let mut guard = my_mutex.lock().unwrap(); 58 *guard = 6; // modify protected data 59 } // guard dropped here, mutex unlocked 60 61 println!("{:?}", my_mutex); // prints: Mutex { data: 6 } 62 } 63 ``` 64 65 ## Poisoning 66 67 - If a thread panics while holding the lock, the mutex becomes poisoned to 68 signal that the data may be in an inconsistent state. 69 70 - After that, `.lock()` returns an `Err(PoisonError)` instead of 71 `Ok(MutexGuard)`; you often handle this with `unwrap()` (propagating the 72 panic) or `unwrap_or_else` to recover. 73 74 ### Question: whats the difference between Arc and Mutex? 75 76 Arc and Mutex solve different problems: Arc gives shared ownership of data 77 across threads, while Mutex controls exclusive access to data (usually for 78 mutation) so only one thread touches it at a time. 79 80 ## How Arc and Mutex are used together 81 82 A very common pattern in Rust is Arc<Mutex<T>>, where: 83 84 - Arc lets many threads share ownership of the same Mutex<T>. 85 86 - Mutex ensures that each access to T is exclusive, so concurrent mutation is 87 safe. 88 89 ## Quick example intuition 90 91 If you want several threads to read the same configuration that never changes, 92 you typically use Arc<Config>. 93 94 If you want several threads to increment a shared counter, you typically use 95 Arc<Mutex<u32>> so they can all own the counter and take turns mutating it 96 safely. 97 98 ## On using `std::sync::Mutex` and `tokio::sync::Mutex` 99 100 > Note that std::sync::Mutex and not tokio::sync::Mutex is used to guard the 101 > HashMap. A common error is to unconditionally use tokio::sync::Mutex from 102 > within async code. An async mutex is a mutex that is locked across calls to 103 > .await. 104 105 > A synchronous mutex will block the current thread when waiting to acquire the 106 > lock. This, in turn, will block other tasks from processing. Switching to 107 > tokio::sync::Mutex will cause the task to yield control back to the executor, 108 > but this will usually not help with performance as the asynchronous mutex uses 109 > a synchronous mutex internally. 110 111 > As a rule of thumb, using a synchronous mutex from within asynchronous code is 112 > fine as long as contention remains low and the lock is not held across calls 113 > to .await. 114 115 ### Question: What does “Locked across calls to .await” mean? 116 117 It means: you acquired the mutex lock before an `.await`, and you still hold 118 that lock while the future is suspended at that `.await` and possibly resumed 119 later on another poll. 120 121 In async Rust, every .await is a point where your function can be suspended and 122 other tasks can run on the same thread. 123 124 Code that hold lock across the await: 125 126 ```rust 127 async fn f(m: &std::sync::Mutex<Data>) { 128 let mut guard = m.lock().unwrap(); // lock acquired here 129 130 do_something_async().await; // <-- await while still holding `guard` 131 132 guard.value += 1; // lock released only when `guard` is dropped 133 } 134 ``` 135 136 the Mutex remains locked for the entire time between taking guard and dropping 137 it, including the whole period when the function is paused at 138 do_something_async().await. The lock is “held across the await”. 139 [How to Use async Rust Without Blocking the RUntime](https://oneuptime.com/blog/post/2026-01-07-rust-async-without-blocking/view) 140 141 By contrast, code that does not hold the lock across an .await acquires and 142 releases it entirely before the first .await: 143 144 ```rust 145 async fn g(m: &std::sync::Mutex<Data>) { 146 { 147 let mut guard = m.lock().unwrap(); 148 guard.counter += 1; 149 // lock is dropped at the end of this block 150 } // <--- lock released here 151 152 do_something_async().await; // no lock held during await 153 } 154 ``` 155 156 Here the critical section is synchronous and short, so using std::sync::Mutex is 157 fine because the lock is never held while the task is suspended at .await. 158 [Sync mutex in async program](https://users.rust-lang.org/t/sync-mutex-in-async-program/66118) 159 160 A quick way to think about it: 161 162 - “Held/locked across `.await`” = the lifetime of your `MutexGuard` spans over 163 an `.await` expression. 164 165 - “Not held across `.await`” = you drop the guard (end its scope) before you hit 166 any `.await`. 167 168 However, the `tokio::sync::Mutex` is designed to be held across `await` points: 169 170 ```rust 171 async fn g(m: &tokio::sync::Mutex<Data>) { 172 let mut guard = m.lock().await; // yields if needed, no thread block 173 do_something_async().await; // still holding the lock 174 guard.counter += 1; 175 } // lock released when guard is dropped 176 ``` 177 178 is legal in terms of the runtime: you’re not blocking a worker thread while 179 waiting for I/O, other tasks can still run. 180 181 So “locked across calls to .await” means: 182 183 - With std::sync::Mutex: strongly discouraged; can stall threads and deadlock. 184 185 - With tokio::sync::Mutex: allowed and supported; the runtime knows how to park 186 and wake tasks that are waiting on the async mutex. 187 188 #### When to choose which 189 190 - Use std::sync::Mutex in async code when: 191 192 - You only lock for very short, synchronous work. 193 194 - You always drop the guard before any .await. 195 196 - Use tokio::sync::Mutex when: 197 198 - You really need to keep the lock across one or more .awaits, or 199 200 - The critical section involves async operations that can’t be easily 201 refactored to be purely synchronous. 202 203 ``` 204 ## Reference 205 206 [Spinlock Considered Harmful](https://matklad.github.io/2020/01/02/spinlocks-considered-harmful.html) 207 ```