Basics of Rust Concurrency Long before multi-core processors were commonplace, operating systems allowed for a single computer to run many programs concurrently. This is achieved by rapidly switching between processes, allowing each to repeatedly make a little bit of progress, one by one. Nowadays, virtually all our computers and even our phones and watches have processors with multiple cores, which can truly execute multiple processes in parallel. Operating systems isolate processes from each other as much as possible, allowing a program to do its thing while completely unaware of what any other processes are doing. For example, a process cannot normally access the memory of another process, or communicate with it in any way, without asking the operating system’s kernel first. 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. This chapter will explain how threads are spawned in Rust, and all the basic concepts around them, such as how to safely share data between multiple threads. The con‐ cepts explained in this chapter are foundational to the rest of the book. If you’re already familiar with these parts of Rust, feel free to skip ahead. However, before you continue to the next chapters, make sure you have a good understanding of threads, interior mutability, Send and Sync, and know what a mutex, a condition variable, and thread parking are. 1 Threads in Rust 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. 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. The thread stops once this function returns. Let’s take a look at an example: use std::thread; fn main() { thread::spawn(f); thread::spawn(f); println!("Hello from the main thread."); } fn f() { println!("Hello from another thread!"); let id = thread::current().id(); println!("This is my thread id: {id:?}"); } We spawn two threads that will both execute f as their main function. Both of these threads will print a message and show their thread id, while the main thread will also print its own message. Thread ID 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. If you run our example program above several times, you might notice the out‐ put varies between runs. This is the output I got on my machine during one particular run: Hello from the main thread. Hello from another thread! This is my thread id: Surprisingly, part of the output seems to be missing. 2 | Chapter 1: Basics of Rust Concurrency What happened here is that the main thread finished executing the main function before the newly spawned threads finished executing their functions. Returning from main will exit the entire program, even if other threads are still running. In this particular example, one of the newly spawned threads had just enough time to get to halfway through the second message, before the program was shut down by the main thread. If we want to make sure the threads are finished before we return from main, we can wait for them by joining them. To do so, we have to use the JoinHandle returned by the spawn function: fn main() { let t1 = thread::spawn(f); let t2 = thread::spawn(f); println!("Hello from the main thread."); t1.join().unwrap(); t2.join().unwrap(); } The .join() method waits until the thread has finished executing and returns a std::thread::Result. If the thread did not successfully finish its function because it panicked, this will contain the panic message. We could attempt to handle that situation, or just call .unwrap() to panic when joining a panicked thread. Running this version of our program will no longer result in truncated output: Hello from the main thread. Hello from another thread! This is my thread id: ThreadId(3) Hello from another thread! This is my thread id: ThreadId(2) The only thing that still changes between runs is the order in which the messages are printed: Hello from the main thread. Hello from another thread! Hello from another thread! This is my thread id: ThreadId(2) This is my thread id: ThreadId(3) Threads in Rust | 3 Output Locking 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) Rather than passing the name of a function to std::thread::spawn, as in our exam‐ ple above, it’s far more common to pass it a closure. This allows us to capture values to move into the new thread: let numbers = vec![1, 2, 3]; thread::spawn(move || { for n in numbers { println!("{n}"); } }).join().unwrap(); Here, ownership of numbers is transferred to the newly spawned thread, since we used a move closure. If we had not used the move keyword, the closure would have captured numbers by reference. This would have resulted in a compiler error, since the new thread might outlive that variable. 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. A closure capturing a local variable by reference may not be kept around forever, since that reference would become invalid the moment the local variable ceases to exist. Getting a value back out of the thread is done by returning it from the closure. This return value can be obtained from the Result returned by the join method: let numbers = Vec::from_iter(0..=1000); let t = thread::spawn(move || { let len = numbers.len(); let sum = numbers.into_iter().sum::(); sum / len }); let average = t.join().unwrap(); 4 | Chapter 1: Basics of Rust Concurrency println!("average: {average}"); Here, the value returned by the thread’s closure ( ) is sent back to the main thread through the join method ( ). If numbers had been empty, the thread would’ve panicked while trying to divide by zero ( ), and join would’ve returned that panic message instead, causing the main thread to panic too because of unwrap ( ). Thread Builder The std::thread::spawn function is actually just a convenient shorthand for std::thread::Builder::new().spawn().unwrap(). A std::thread::Builder allows you to set some settings for the new thread before spawning it. You can use it to configure the stack size for the new thread and to give the new thread a name. The name of a thread is available through std::thread::cur rent().name(), will be used in panic messages, and will be visible in monitoring and debugging tools on most platforms. Additionally, Builder’s spawn function returns an std::io::Result, allowing you to handle situations where spawning a new thread fails. This might happen if the operating system runs out of memory, or if resource limits have been applied to your program. The std::thread::spawn function simply panics if it is unable to spawn a new thread. Scoped Threads If we know for sure that a spawned thread will definitely not outlive a certain scope, that thread could safely borrow things that do not live forever, such as local variables, as long as they outlive that scope. 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. How it works is best shown with an example: let numbers = vec![1, 2, 3]; thread::scope(|s| { s.spawn(|| { println!("length: {}", numbers.len()); }); s.spawn(|| { for n in &numbers { Scoped Threads | 5 println!("{n}"); } }); }); We call the std::thread::scope function with a closure. Our closure is directly executed and gets an argument, s, representing the scope. We use s to spawn threads. The closures can borrow local variables like numbers. When the scope ends, all threads that haven’t been joined yet are automatically joined. 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. In the example above, both of the new threads are concurrently accessing numbers. This is fine, because neither of them (nor the main thread) modifies it. If we were to change the first thread to modify numbers, as shown below, the compiler wouldn’t allow us to spawn another thread that also uses numbers: let mut numbers = vec![1, 2, 3]; thread::scope(|s| { s.spawn(|| { numbers.push(1); }); s.spawn(|| { numbers.push(2); // Error! }); }); The exact error message depends on the version of the Rust compiler, since it’s often improved to produce better diagnostics, but attempting to compile the code above will result in something like this: error[E0499]: cannot borrow `numbers` as mutable more than once at a time --> example.rs:7:13 | 4 | s.spawn(|| { | -- first mutable borrow occurs here 5 | numbers.push(1); | ------- first borrow occurs due to use of `numbers` in closure | 7 | s.spawn(|| { | ^^ second mutable borrow occurs here 8 | numbers.push(2); | ------- second borrow occurs due to use of `numbers` in closure 6 | Chapter 1: Basics of Rust Concurrency The Leakpocalypse Before Rust 1.0, the standard library had a function named std::thread::scoped that would directly spawn a thread, just like std::thread::spawn. It allowed non-'static captures, because instead of a JoinHandle, it returned a JoinGuard which joined the thread when dropped. Any borrowed data only needed to outlive this JoinGuard. This seemed safe, as long as the JoinGuard got dropped at some point. Just before the release of Rust 1.0, it slowly became clear that it’s not possible to guarantee that something will be dropped. There are many ways, such as creating a cycle of reference-counted nodes, that make it possible to forget about something, or leak it, without dropping it. Eventually, in what some people refer to as “The Leakpocalypse,” the conclusion was made that the design of a (safe) interface cannot rely on the assumption that objects will always be dropped at the end of their lifetime. Leaking an object might reasonably result in leaking more objects (e.g., leaking a Vec will also leak its ele‐ ments), but it may not result in undefined behavior. Because of this conclusion, std::thread::scoped was no longer deemed safe and was removed from the stan‐ dard library. Additionally, std::mem::forget was upgraded from an unsafe function to a safe function, to emphasize that forgetting (or leaking) is always a possibility. Only much later, in Rust 1.63, a new std::thread::scoped function was added with a new design that does not rely on Drop for correctness. Shared Ownership and Reference Counting So far we’ve looked at transferring ownership of a value to a thread using a move closure (“Threads in Rust” on page 2) and borrowing data from longer-living parent threads (“Scoped Threads” on page 5). When sharing data between two threads where neither thread is guaranteed to outlive the other, neither of them can be the owner of that data. Any data shared between them will need to live as long as the longest living thread. Statics There are several ways to create something that’s not owned by a single thread. The simplest one is a static value, which is “owned” by the entire program, instead of an individual thread. In the following example, both threads can access X, but neither of them owns it: static X: [i32; 3] = [1, 2, 3]; Shared Ownership and Reference Counting | 7 thread::spawn(|| dbg!(&X)); thread::spawn(|| dbg!(&X)); A static item has a constant initializer, is never dropped, and already exists before the main function of the program even starts. Every thread can borrow it, since it’s guaranteed to always exist. Leaking 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. let x: &'static [i32; 3] = Box::leak(Box::new([1, 2, 3])); thread::spawn(move || dbg!(x)); thread::spawn(move || dbg!(x)); The move closure might make it look like we’re moving ownership into the threads, but a closer look at the type of x reveals that we’re only giving the threads a reference to the data. References are Copy, meaning that when you “move” them, the original still exists, just like with an integer or boolean. 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. The past is simply not relevant. The downside of leaking a Box is that we’re leaking memory. We allocate something, but never drop and deallocate it. This can be fine if it happens only a limited number of times. But if we keep doing this, the program will slowly run out of memory. Reference Counting To make sure that shared data gets dropped and deallocated, we can’t completely give up its ownership. Instead, 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. The Rust standard library provides this functionality through the std::rc::Rc type, short for “reference counted.” It is very similar to a Box, except cloning it will not allocate anything new, but instead increment a counter stored next to the contained 8 | Chapter 1: Basics of Rust Concurrency value. Both the original and cloned Rc will refer to the same allocation; they share ownership. use std::rc::Rc; let a = Rc::new([1, 2, 3]); let b = a.clone(); assert_eq!(a.as_ptr(), b.as_ptr()); // Same allocation! Dropping an Rc will decrement the counter. Only the last Rc, which will see the counter drop to zero, will be the one dropping and deallocating the contained data. If we were to try to send an Rc to another thread, however, we would run into the following compiler error: error[E0277]: `Rc` cannot be sent between threads safely | 8 | thread::spawn(move || dbg!(b)); | ^^^^^^^^^^^^^^^ As it turns out, Rc is not thread safe (more on that in “Thread Safety: Send and Sync” on page 16). 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. Instead, 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. (More on that in Chapter 2.) use std::sync::Arc; let a = Arc::new([1, 2, 3]); let b = a.clone(); thread::spawn(move || dbg!(a)); thread::spawn(move || dbg!(b)); We put an array in a new allocation together with a reference counter, which starts at one. Cloning the Arc increments the reference count to two and provides us with a second Arc to the same allocation. Both threads get their own Arc through which they can access the shared array. Both decrement the reference counter when they drop their Arc. The last thread to drop its Arc will see the counter drop to zero and will be the one to drop and deallocate the array. Shared Ownership and Reference Counting | 9 Naming Clones Having to give every clone of an Arc a different name can quickly make the code quite cluttered and hard to follow. While every clone of an Arc is a separate object, each clone represents the same shared value, which is not well reflected by naming each one differently. 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. But by opening a new scope, a statement like let a = a.clone(); can be used to reuse the same name within that scope, while leaving the original variable available outside the scope. By wrapping a closure in a new scope (with {}), we can clone variables before moving them into the closure, without having to rename them. let a = Arc::new([1, 2, 3]); let b = a.clone(); thread::spawn(move || { dbg!(b); }); dbg!(a); The clone of the Arc lives in the same scope. Each thread gets its own clone with a different name. let a = Arc::new([1, 2, 3]); thread::spawn({ let a = a.clone(); move || { dbg!(a); } }); dbg!(a); The clone of the Arc lives in a different scope. We can use the same name in each thread. Because ownership is shared, reference counting pointers (Rc and Arc) have the same restrictions as shared references (&T). They do not give you mutable access to their contained value, since the value might be borrowed by other code at the same time. For example, if we were to try to sort the slice of integers in an Arc<[i32]>, the compiler would stop us from doing so, telling us that we’re not allowed to mutate the data: error[E0596]: cannot borrow data in an `Arc` as mutable | 6 | a.sort(); | ^^^^^^^^ 10 | Chapter 1: Basics of Rust Concurrency Borrowing and Data Races In Rust, values can be borrowed in two ways: Immutable borrowing 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. As the name implies, the compiler doesn’t normally allow you to mutate something through such a reference, since that might affect other code that’s currently borrowing the same data. Mutable borrowing Borrowing something with &mut gives a mutable reference. A mutable borrow guarantees it’s the only active borrow of that data. This ensures that mutating the data will not change anything that other code is currently looking at. These two concepts together fully prevent data races: situations where one thread is mutating data while another is concurrently accessing it. Data races are generally undefined behavior, which means the compiler does not need to take these situations into account. It will simply assume they do not happen. To clarify what that means, let’s take a look at an example where the compiler can make a useful assumption using the borrowing rules: fn f(a: &i32, b: &mut i32) { let before = *a; *b += 1; let after = *a; if before != after { x(); // never happens } } Here, we get an immutable reference to an integer, and store the value of the integer both before and after incrementing the integer that b refers to. The compiler is free to assume that the fundamental rules about borrowing and data races are upheld, which means that b can’t possibly refer to the same integer as a does. In fact, nothing in the entire program can mutably borrow the integer that a refers to as long as a is borrowing it. Therefore, the compiler can easily conclude that *a will not change and the condition of the if statement will never be true, and can completely remove the call to x from the program as an optimization. It’s impossible to write a Rust program that breaks the compiler’s assumptions, other than by using an unsafe block to disable some of the compiler’s safety checks. Borrowing and Data Races | 11 Undefined Behavior Languages like C, C++, and Rust have a set of rules that need to be followed to avoid something called undefined behavior. For example, one of Rust’s rules is that there may never be more than one mutable reference to any object. In Rust, it’s only possible to break any of these rules when using unsafe code. “Unsafe” doesn’t mean that the code is incorrect or never safe to use, but rather that the compiler is not validating for you that the code is safe. If the code does violate these rules, it is called unsound. The compiler is allowed to assume, without checking, that these rules are never broken. When broken, this results in something called undefined behavior, which we need to avoid at all costs. If we allow the compiler to make an assumption that is not actually true, it can easily result in more wrong conclusions about different parts of your code, affecting your whole program. As a concrete example, let’s take a look at a small snippet that uses the get_unchecked method on a slice: let a = [123, 456, 789]; let b = unsafe { a.get_unchecked(index) }; The get_unchecked method gives us an element of the slice given its index, just like a[index], but allows the compiler to assume the index is always within bounds, without any checks. This mean that in this code snippet, because a is of length 3, the compiler may assume that index is less than three. It’s up to us to make sure its assumption holds. If we break this assumption, for example if we run this with index equal to 3, anything might happen. It might result in reading from memory whatever was stored in the bytes right after a. It might cause the program to crash. It might end up executing some entirely unrelated part of the program. It can cause all kinds of havoc. Perhaps surprisingly, undefined behavior can even “travel back in time,” causing problems in code that precedes it. To understand how that can happen, imagine we had a match statement before our previous snippet, as follows: match index { 0 => x(), 1 => y(), _ => z(index), } let a = [123, 456, 789]; let b = unsafe { a.get_unchecked(index) }; Because of the unsafe code, the compiler is allowed to assume index is only ever 0, 1, or 2. It may logically conclude that the last arm of our match statement will only 12 | Chapter 1: Basics of Rust Concurrency ever match a 2, and thus that z is only ever called as z(2). That conclusion might be used not only to optimize the match, but also to optimize z itself. This can include throwing out unused parts of the code. If we execute this with an index of 3, our program might attempt to execute parts that have been optimized away, resulting in completely unpredictable behavior, long before we get to the unsafe block on the last line. Just like that, undefined behavior can propagate through a whole program, both backwards and forwards, in often very unexpected ways. When calling any unsafe function, read its documentation carefully and make sure you fully understand its safety requirements: the assumptions you need to uphold, as the caller, to avoid undefined behavior. Interior Mutability The borrowing rules as introduced in the previous section are simple, but can be quite limiting—especially when multiple threads are involved. Following these rules makes communication between threads extremely limited and almost impossible, since no data that’s accessible by multiple threads can be mutated. Luckily, there is an escape hatch: interior mutability. A data type with interior muta‐ bility slightly bends the borrowing rules. Under certain conditions, those types can allow mutation through an “immutable” reference. In “Reference Counting” on page 8, we’ve already seen one subtle example involving interior mutability. Both Rc and Arc mutate a reference counter, even though there might be multiple clones all using the same reference counter. As soon as interior mutable types are involved, calling a reference “immutable” or “mutable” becomes confusing and inaccurate, since some things can be mutated through both. The more accurate terms are “shared” and “exclusive”: a shared refer‐ ence (&T) can be copied and shared with others, while an exclusive reference (&mut T) guarantees it’s the only exclusive borrowing of that T. For most types, shared references do not allow mutation, but there are exceptions. Since in this book we will mostly be working with these exceptions, we’ll use the more accurate terms in the rest of this book. Keep in mind that interior mutability only bends the rules of shared borrowing to allow mutation when shared. It does not change anything about exclusive borrowing. Exclusive borrowing still guarantees that there are no other active borrows. Unsafe code that results in more than one active exclusive reference to some‐ thing always invokes undefined behavior, regardless of interior mutability. Interior Mutability | 13 Let’s take a look at a few types with interior mutability and how they can allow mutation through shared references without causing undefined behavior. Cell A std::cell::Cell simply wraps a T, but allows mutations through a shared reference. To avoid undefined behavior, it only allows you to copy the value out (if T is Copy), or replace it with another value as a whole. In addition, it can only be used within a single thread. Let’s take a look at an example similar to the one in the previous section, but this time using Cell instead of i32: use std::cell::Cell; fn f(a: &Cell, b: &Cell) { let before = a.get(); b.set(b.get() + 1); let after = a.get(); if before != after { x(); // might happen } } Unlike last time, it is now possible for the if condition to be true. Because a Cell has interior mutability, the compiler can no longer assume its value won’t change as long as we have a shared reference to it. Both a and b might refer to the same value, such that mutating through b might affect a as well. It may still assume, however, that no other threads are accessing the cells concurrently. The restrictions on a Cell are not always easy to work with. Since it can’t directly let us borrow the value it holds, we need to move a value out (leaving something in its place), modify it, then put it back, to mutate its contents: fn f(v: &Cell>) { let mut v2 = v.take(); // Replaces the contents of the Cell with an empty Vec v2.push(1); v.set(v2); // Put the modified Vec back } RefCell Unlike a regular Cell, a std::cell::RefCell does allow you to borrow its contents, at a small runtime cost. A RefCell does not only hold a T, but also holds a counter that keeps track of any outstanding borrows. If you try to borrow it while it is already mutably borrowed (or vice-versa), it will panic, which avoids undefined behavior. Just like a Cell, a RefCell can only be used within a single thread. 14 | Chapter 1: Basics of Rust Concurrency Borrowing the contents of RefCell is done by calling borrow or borrow_mut: use std::cell::RefCell; fn f(v: &RefCell>) { v.borrow_mut().push(1); // We can modify the `Vec` directly. } While Cell and RefCell can be very useful, they become rather useless when we need to do something with multiple threads. So let’s move on to the types that are relevant for concurrency. Mutex and RwLock An RwLock or reader-writer lock is the concurrent version of a RefCell. An RwLock 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. We’ll just have to patiently wait for our turn with the data, after the other threads are done with it. Borrowing the contents of an RwLock is called locking. By locking it we temporarily block concurrent conflicting borrows, allowing us to borrow it without causing data races. 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. We’ll go more into detail on these types in “Locking: Mutexes and RwLocks” on page 18.

****************


Q- so this my notes from the book called “Rust Atomics and locks “ 
Q- you can see the notes introduce the basci concept here 
Q- so using only this context give me 
Q- 20 fact using question & answer style oky