notes

Log | Files | Refs | README

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 ```