# Rust Atomics and Locks ![rw-book-cover](https://m.media-amazon.com/images/I/81WpA78jtqL._SY160.jpg) ## Metadata - Author: Mara Bos - Full Title: Rust Atomics and Locks - Category: #rust #multi-threading #async-programming ## Highlights - #[allow(unused)] use std::{     cell::{Cell, RefCell, UnsafeCell},     collections::VecDeque,     marker::PhantomData,     mem::{ManuallyDrop, MaybeUninit},     ops::{Deref, DerefMut},     ptr::NonNull,     rc::Rc,     sync::{*, atomic::{*, Ordering::*}},     thread::{self, Thread}, }; ([Location 130](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=130)) - However, a program can spawn extra threads of execution, as part of the same process. Threads within the same process are not isolated from each other. Threads share memory and can interact with each other through that memory. ([Location 204](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=204)) - Every program starts with exactly one thread: the main thread. This thread will execute your main function and can be used to spawn more threads if necessary. ([Location 213](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=213)) - In Rust, new threads are spawned using the std::thread::spawn function from the standard library. It takes a single argument: the function the new thread will execute. ([Location 216](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=216)) - The Rust standard library assigns every thread a unique identifier. This identifier is accessible through Thread::id() and is of the type ThreadId. There’s not much you can do with a ThreadId other than copying it around and checking for equality. There is no guarantee that these IDs will be assigned consecutively, only that they will be different for each thread. ([Location 254](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=254)) - If we want to make sure the threads are finished before we return from main, we can wait for them by joining them. ([Location 268](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=268)) - The .join() method waits until the thread has finished executing and returns a std::thread::Result. ([Location 298](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=298)) - The println macro uses std::io::Stdout::lock() to make sure its output does not get interrupted. A println!() expression will wait until any concurrently running one is finished before writing any output. If this was not the case, we could’ve gotten more interleaved output such as: Hello fromHello from another thread!  another This is my threthreadHello fromthread id: ThreadId! ( the main thread. 2)This is my thread id: ThreadId(3) ([Location 311](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=311)) - Rather than passing the name of a function to std::thread::spawn, as in our example above, it’s far more common to pass it a closure. ([Location 317](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=317)) - Since a thread might run until the very end of the program’s execution, the spawn function has a 'static lifetime bound on its argument type. In other words, it only accepts functions that may be kept around forever. ([Location 349](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=349)) - The std::thread::spawn function is actually just a convenient shorthand for std::thread::Builder::new().spawn().unwrap(). ([Location 417](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=417)) - The Rust standard library provides the std::thread::scope function to spawn such scoped threads. It allows us to spawn threads that cannot outlive the scope of the closure we pass to that function, making it possible to safely borrow local variables. ([Location 437](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=437)) - This pattern guarantees that none of the threads spawned in the scope can outlive the scope. Because of that, this scoped spawn method does not have a 'static bound on its argument type, allowing us to reference anything as long as it outlives the scope, such as numbers. ([Location 497](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=497)) - The simplest one is a static value, which is “owned” by the entire program, instead of an individual thread. ([Location 577](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=577)) - Another way to share ownership is by leaking an allocation. Using Box::leak, one can release ownership of a Box, promising to never drop it. From that point on, the Box will live forever, without an owner, allowing it to be borrowed by any thread for as long as the program runs. ([Location 604](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=604)) - Note how the 'static lifetime doesn’t mean that the value lived since the start of the program, but only that it lives to the end of the program. ([Location 636](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=636)) - The downside of leaking a Box is that we’re leaking memory. ([Location 637](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=637)) - we can share ownership. By keeping track of the number of owners, we can make sure the value is dropped only when there are no owners left. ([Location 643](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=643)) - std::rc::Rc type, short for “reference counted.” ([Location 647](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=647)) - Both the original and cloned Rc will refer to the same allocation; they share ownership. ([Location 648](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=648)) - Rc is not thread safe (more on that in “Thread Safety: Send and Sync”). If multiple threads had an Rc to the same allocation, they might try to modify the reference counter at the same time, which can give unpredictable results. ([Location 679](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=679)) - we can use std::sync::Arc, which stands for “atomically reference counted.” It’s identical to Rc, except it guarantees that modifications to the reference counter are indivisible atomic operations, making it safe to use it with multiple threads. ([Location 683](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=683)) - Rust allows (and encourages) you to shadow variables by defining a new variable with the same name. If you do that in the same scope, the original variable cannot be named anymore. ([Location 741](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=741)) - Borrowing something with & gives an immutable reference. Such a reference can be copied. Access to the data it references is shared between all copies of such a reference. ([Location 809](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=809)) - Borrowing something with &mut gives a mutable reference. ([Location 815](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=815)) - Luckily, there is an escape hatch: interior mutability. A data type with interior mutability slightly bends the borrowing rules. ([Location 953](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=953)) - The more accurate terms are “shared” and “exclusive”: a shared reference (&T) can be copied and shared with others, while an exclusive reference (&mut T) guarantees it’s the only exclusive borrowing of that T. ([Location 963](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=963)) - A std::cell::Cell<T> simply wraps a T, but allows mutations through a shared reference. ([Location 976](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=976)) - Unlike a regular Cell, a std::cell::RefCell does allow you to borrow its contents, at a small runtime cost. A RefCell<T> does not only hold a T, but also holds a counter that keeps track of any outstanding borrows. ([Location 1052](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1052)) - An RwLock or reader-writer lock is the concurrent version of a RefCell. An RwLock<T> holds a T and tracks any outstanding borrows. However, unlike a RefCell, it does not panic on conflicting borrows. Instead, it blocks the current thread—​putting it to sleep—​while waiting for conflicting borrows to disappear. ([Location 1080](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1080)) - A Mutex is very similar, but conceptually slightly simpler. Instead of keeping track of the number of shared and exclusive borrows like an RwLock, it only allows exclusive borrows. ([Location 1088](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1088)) - Like a Cell, they avoid undefined behavior by making us copy values in and out as a whole, without letting us borrow the contents directly. ([Location 1096](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1096)) - An UnsafeCell is the primitive building block for interior mutability. ([Location 1108](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1108)) - All types with interior mutability—​including all types discussed above—​are built on top of UnsafeCell. ([Location 1115](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1115)) - The language uses two special traits to keep track of which types can be safely used across threads: Send A type is Send if it can be sent to another thread. In other words, if ownership of a value of that type can be transferred to another thread. For example, Arc<i32> is Send, but Rc<i32> is not. Sync A type is Sync if it can be shared with another thread. In other words, a type T is Sync if and only if a shared reference to that type, &T, is Send. For example, an i32 is Sync, but a Cell<i32> is not. (A Cell<i32> is Send, however.) ([Location 1123](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1123)) - All primitive types such as i32, bool, and str are both Send and Sync. ([Location 1137](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1137)) - The way to opt out of either of these is to add a field to your type that does not implement the trait. For that purpose, the special std::marker::PhantomData<T> type often comes in handy. ([Location 1144](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1144)) - Raw pointers (*const T and *mut T) are neither Send nor Sync, since the compiler doesn’t know much about what they represent. ([Location 1169](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1169)) - The thread::spawn function requires its argument to be Send, and a closure is only Send if all of its captures are. ([Location 1225](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1225)) - The most commonly used tool for sharing (mutable) data between threads is a mutex, which is short for “mutual exclusion.” ([Location 1232](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1232)) - Conceptually, a mutex has only two states: locked and unlocked. ([Location 1235](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1235)) - The Rust standard library provides this functionality through std::sync::Mutex<T>. ([Location 1243](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1243)) - To ensure a locked mutex can only be unlocked by the thread that locked it, it does not have an unlock() method. Instead, its lock() method returns a special type called a MutexGuard. ([Location 1247](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1247)) - Unlocking the mutex is done by dropping the guard. ([Location 1253](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1253)) - After the threads are done, we can safely remove the protection from the integer through into_inner(). The into_inner method takes ownership of the mutex, which guarantees that nothing else can have a reference to the mutex anymore, making locking unnecessary. ([Location 1317](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1317)) - A Mutex in Rust gets marked as poisoned when a thread panics while holding the lock. When that happens, the Mutex will no longer be locked, but calling its lock method will result in an Err to indicate it has been poisoned. ([Location 1457](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1457)) - A reader-writer lock is a slightly more complicated version of a mutex that understands the difference between exclusive and shared access, and can provide either. It has three states: unlocked, locked by a single writer (for exclusive access), and locked by any number of readers (for shared access). ([Location 1555](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1555)) - It is effectively the multi-threaded version of RefCell, dynamically tracking the number of references to ensure the borrow rules are upheld. ([Location 1570](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1570)) - This is done to prevent writer starvation, a situation where many readers collectively keep the lock from ever unlocking, never allowing any writer to update the data. ([Location 1589](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1589)) - While a mutex does allow threads to wait until it becomes unlocked, it does not provide functionality for waiting for any other conditions. ([Location 1611](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1611)) - One way to wait for a notification from another thread is called thread parking. A thread can park itself, which puts it to sleep, stopping it from consuming any CPU cycles. Another thread can then unpark the parked thread, waking it up from its nap. ([Location 1618](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1618)) - An important property of thread parking is that a call to unpark() before the thread parks itself does not get lost. The request to unpark is still recorded, and the next time the thread tries to park itself, it clears that request and directly continues without actually going to sleep. ([Location 1716](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1716)) - However, unpark requests don’t stack up. Calling unpark() two times and then calling park() two times afterwards still results in the thread going to sleep. The first park() clears the request and returns directly, but the second one goes to sleep as usual. ([Location 1731](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1731)) - Condition variables are a more commonly used option for waiting for something to happen to data protected by a mutex. They have two basic operations: wait and notify. ([Location 1748](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1748)) - The Rust standard library provides a condition variable as std::sync::Condvar. Its wait method takes a MutexGuard that proves we’ve locked the mutex. It first unlocks the mutex and goes to sleep. Later, when woken up, it relocks the mutex and returns a new MutexGuard (which proves that the mutex is locked again). ([Location 1757](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1757)) - Both thread::park() and Condvar::wait() also have a variant with a time limit: thread::park_timeout() and Condvar::wait_timeout(). These take a Duration as an extra argument, which is the time after which it should give up waiting for a notification and unconditionally wake up. ([Location 1871](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1871)) - Multiple threads can run concurrently within the same program and can be spawned at any time. ([Location 1880](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1880)) - When the main thread ends, the entire program ends. ([Location 1881](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1881)) - Data races are undefined behavior, which is fully prevented (in safe code) by Rust’s type system. ([Location 1882](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1882)) - Data that is Send can be sent to other threads, and data that is Sync can be shared between threads. ([Location 1883](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1883)) - Regular threads might run as long as the program does, and thus can only borrow 'static data such as statics and leaked allocations. ([Location 1884](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1884)) - Reference counting (Arc) can be used to share ownership to make sure data lives as long as at least one thread is using it. ([Location 1886](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1886)) - Scoped threads are useful to limit the lifetime of a thread to alllow it to borrow non-'static data, such as local variables. ([Location 1888](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1888)) - &T is a shared reference. &mut T is an exclusive reference. Regular types do not allow mutation through a shared reference. ([Location 1889](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1889)) - Some types have interior mutability, thanks to UnsafeCell, which allows for mutation through shared references. ([Location 1891](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1891)) - Cell and RefCell are the standard types for single-threaded interior mutability. Atomics, Mutex, and RwLock are their multi-threaded equivalents. ([Location 1893](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1893)) - Cell and atomics only allow replacing the value as a whole, while RefCell, Mutex, and RwLock allow you to mutate the value directly by dynamically enforcing access rules. ([Location 1896](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1896)) - Thread parking can be a convenient way to wait for some condition. ([Location 1899](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1899)) - When a condition is about data protected by a Mutex, using a Condvar is more convenient, and can be more efficient, than thread parking. ([Location 1899](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1899)) - In computer science, it is used to describe an operation that is indivisible: it is either fully completed, or it didn’t happen yet. ([Location 1908](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1908)) - Atomic operations are the main building block for anything involving multiple threads. All the other concurrency primitives, such as mutexes and condition variables, are implemented using atomic operations. ([Location 1914](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1914)) - Unlike most types, they allow modification through a shared reference (e.g., &AtomicU8). This is possible thanks to interior mutability, ([Location 1932](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1932)) - memory ordering: Every atomic operation takes an argument of type std::sync::atomic::Ordering, which determines what guarantees we get about the relative ordering of operations. The simplest variant with the fewest guarantees is Relaxed. Relaxed still guarantees consistency on a single atomic variable, but does not promise anything about the relative order of operations between different variables. ([Location 1938](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1938)) - The first two atomic operations we’ll look at are the most basic ones: load and store. Their function signatures are as follows, using AtomicI32 as an example: impl AtomicI32 {     pub fn load(&self, ordering: Ordering) -> i32;     pub fn store(&self, value: i32, ordering: Ordering); } ([Location 1956](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=1956)) - This time, we use a scoped thread (“Scoped Threads”), which will automatically handle the joining of the thread for us, and also allow us to borrow local variables. ([Location 2153](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=2153)) - Once the last item is processed, it might take up to one whole second for the main thread to know, introducing an unnecessary delay at the end. To solve this, we can use thread parking (“Thread Parking”) to wake the main thread from its sleep whenever there is new information it might be interested in. ([Location 2159](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=2159)) - The main thread now uses park_timeout rather than sleep, such that it can be interrupted. ([Location 2244](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=2244)) - lazy initialization. ([Location 2254](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=2254)) - Since we don’t expect it to change, we can request or calculate it only the first time we need it, and remember the result. The first thread that needs it will have to calculate the value, but it can store it in an atomic static to make it available for all threads, including itself if it needs it again later. ([Location 2258](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=2258)) - One of the threads will end up overwriting the result of the other, depending on which one finishes first. This is called a race. Not a data race, which is undefined behavior and impossible in Rust without using unsafe, but still a race with an unpredictable winner. ([Location 2305](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=2305)) - If calculate_x() is expected to take a long time, it’s better if threads wait while the first thread is still initializing X, to avoid unnecessarily wasting processor time. You could implement this using a condition variable or thread parking ([Location 2311](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=2311)) - the fetch-and-modify operations. These operations modify the atomic variable, but also load (fetch) the original value, as a single atomic operation. ([Location 2325](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=2325)) - The one outlier is the operation that simply stores a new value, regardless of the old value. Instead of fetch_store, it has been called swap. ([Location 2426](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=2426)) - An important thing to keep in mind is that fetch_add and fetch_sub implement wrapping behavior for overflows. ([Location 2468](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=2468)) - To make that work, we can no longer use the store method, as that would overwrite the progress from other threads. Instead, we can use an atomic add operation to increment the counter after every processed item. ([Location 2483](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=2483)) - As a move closure, it moves (or copies) its captures rather than borrowing them, giving it a copy of t. ([Location 2574](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=2574)) - because the Relaxed memory ordering gives no guarantees about the relative order of operations as seen from another thread, it might even briefly see a new updated value of total_time, while still seeing an old value of num_done, resulting in an overestimate of the average. ([Location 2755](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=2755)) - If we want to avoid this, we can put the three statistics inside a Mutex. Then we’d briefly lock the mutex while updating the three numbers, which no longer have to be atomic by themselves. This effectively turns the three updates into a single atomic operation, at the cost of locking and unlocking a mutex, and potentially temporarily blocking threads. ([Location 2760](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=2760)) - The most advanced and flexible atomic operation is the compare-and-exchange operation. This operation checks if the atomic value is equal to a given value, and only if that is the case does it replace it with a new value, all atomically as a single operation. It will return the previous value and tell us whether it replaced it or not. ([Location 2892](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=2892)) - Next to compare_exchange, there is a similar method named compare_exchange_weak. The difference is that the weak version may still sometimes leave the value untouched and return an Err, even though the atomic value matched the expected value. On some platforms, this method can be implemented more efficiently and should be preferred in cases where the consequence of a spurious compare-and-exchange failure are insignificant, such as in our increment function above. ([Location 3051](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=3051)) - we can use compare_exchange to make sure we only store the key if no other thread has already done so, and otherwise throw our key away and use the stored key instead. ([Location 3160](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=3160)) - This is a good example of a situation where compare_exchange is more appropriate than its weak variant. We don’t run our compare-and-exchange operation in a loop, and we don’t want to return zero if the operation spuriously fails. ([Location 3233](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=3233)) - Atomic operations are indivisible; they have either fully completed, or they haven’t happened yet. Atomic operations in Rust are done through the atomic types in std::sync::atomic, such as AtomicI32. Not all atomic types are available on all platforms. The relative ordering of atomic operations is tricky when multiple variables are involved. More in Chapter 3. Simple loads and stores are nice for very basic inter-thread communication, like stop flags and status reporting. Lazy initialization can be done as a race, without causing a data race. Fetch-and-modify operations allow for a small set of basic atomic modifications that are especially useful when multiple threads are modifying the same atomic variable. Atomic addition and subtraction silently wrap around on overflow. Compare-and-exchange operations are the most flexible and general, and a building block for making any other atomic operation. A weak compare-and-exchange operation can be slightly more efficient. ([Location 3245](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=3245)) - Processors and compilers perform all sorts of tricks to make your programs run as fast as possible. A processor might determine that two particular consecutive instructions in your program will not affect each other, and execute them out of order, if that is faster, for example. ([Location 3266](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=3266)) - Let’s take a look at the following function as an example: fn f(a: &mut i32, b: &mut i32) {     *a += 1;     *b += 1;     *a += 1; } Here, the compiler will most certainly understand that the order of these operations does not matter, since nothing happens between these three addition operations that depends on the value of *a or *b. (Assuming overflow checking is disabled.) Because of that, it might reorder the second and third operations, and then merge the first two into a single addition: fn f(a: &mut i32, b: &mut i32) {     *a += 2;     *b += 1; } ([Location 3271](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=3271)) - Instead, we can only pick from a small set of options, represented by the std::sync::atomic::Ordering enum, which every atomic operation takes as an argument. ([Location 3333](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=3333)) - processor and compiler version. The available orderings in Rust are: Relaxed ordering: Ordering::Relaxed Release and acquire ordering: Ordering::{Release, Acquire, AcqRel} Sequentially consistent ordering: Ordering::SeqCst ([Location 3338](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=3338)) - Rust’s memory model, which is mostly copied from C++, doesn’t match any existing processor architecture, but instead is an abstract model with a strict set of rules that attempt to represent the greatest common denominator of all current and future architectures, while also giving the compiler enough freedom to make useful assumptions while analyzing and optimizing programs. ([Location 3349](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=3349)) - The memory model defines the order in which operations happen in terms of happens-before relationships. This means that as an abstract model, it doesn’t talk about machine instructions, caches, buffers, timing, instruction reordering, compiler optimizations, and so on, but instead only defines situations where one thing is guaranteed to happen before another thing, and leaves the order of everything else undefined. ([Location 3364](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=3364)) - Relaxed memory ordering is the most basic (and most performant) memory ordering that, by itself, never results in any cross-thread happens-before relationships. ([Location 3374](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=3374)) - As mentioned above, the basic happens-before rule is that everything that happens within the same thread happens in order. ([Location 3432](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=3432)) - The important and counter-intuitive thing to understand is that operation loading the value 20 does not result in a happens-before relationship with , even though that value is the one stored by . Our intuitive understanding of the concept of “before” breaks down when things don’t necessarily happen in a globally consistent order, such as when instruction reordering is involved. ([Location 3455](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=3455)) - Spawning a thread creates a happens-before relationship between what happened before the spawn() call, and the new thread. Similarly, joining a thread creates a happens-before relationship between the joined thread and what happens after the join() call. ([Location 3465](https://readwise.io/to_kindle?action=open&asin=B0BQ5LN9KC&location=3465))