Rust Concurrency Cheat Sheet
Unlock the power of concurrent programming in Rust, where performance and safety converge. Discover how Rust's unique ownership and borrowing system enables fearless concurrency, guaranteeing memory safety and data race freedom at compile time. Explore essential primitives and patterns for mastering multithreaded code, from threads to parallelism, and supercharge your applications with effortless concurrency and unparalleled efficiency.
1. Introduction
Concurrency is a fundamental aspect of modern programming, enabling applications to perform multiple tasks seemingly simultaneously, thereby improving responsiveness and efficiency. In the context of Rust, a language renowned for its performance and safety, concurrency is not merely an optimization but a core tenet facilitated by its unique ownership and borrowing system 1. This system allows Rust to achieve "fearless concurrency," where developers can write multi-threaded code with a high degree of confidence that memory safety and data race freedom are guaranteed at compile time 1. While concurrency focuses on managing multiple tasks, parallelism involves the actual simultaneous execution of these tasks, often leveraging multi-core processors to accelerate computationally intensive operations 1. This cheat sheet serves as a quick reference for Rust's concurrency features, covering essential primitives and commonly employed patterns.
2. Threads in Rust
-
2.1 Creating and Spawning Threads with std::thread::spawn
The std::thread::spawn() function in Rust's standard library provides a mechanism to create and immediately execute a new, native operating system thread 3. This function accepts a closure as its argument, which encapsulates the code that will be executed on the newly spawned thread 3. For the spawned thread to operate safely and correctly, the closure must implement specific traits: FnOnce() -> T + Send + 'static 5. The FnOnce trait signifies that the closure takes ownership of any captured variables and can be called at least once. The Send trait ensures that the closure and its return value are safe to be moved from one thread to another. The 'static lifetime bound indicates that the closure and its return value must have a lifetime that extends across the entire execution of the program, a necessary condition because threads can outlive the scope in which they were created 5.
Consider the following example that demonstrates the basic usage of
thread::spawn:Rustuse std::thread; use std::time::Duration;fn main() { thread::spawn(|| { for i in 1..5 { println!("Spawned thread: {}", i); thread::sleep(Duration::from_millis(10)); } });
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-number">1</span>..<span class="hljs-number">3</span> { <span class="hljs-built_in">println!</span>(<span class="hljs-string">"Main thread: {}"</span>, i); thread::sleep(Duration::from_millis(<span class="hljs-number">5</span>)); } thread::sleep(Duration::from_millis(<span class="hljs-number">20</span>)); <span class="hljs-comment">// Ensure spawned thread has time to run</span>}In this code, a new thread is spawned that prints numbers from 1 to 4 with a short delay, while the main thread does something similar. The order in which the output from the main thread and the spawned thread appears is not guaranteed, as it depends on how the operating system's thread scheduler allocates CPU time to each thread 7.
-
2.2 Passing Data to Threads
-
Ownership and
moveclosures: By default, Rust closures have the ability to borrow variables from their enclosing scope. However, when dealing with threads, it's often necessary to transfer ownership of data to the spawned thread. This is achieved by using themovekeyword before the closure's parameter list 3. Themovekeyword forces the closure to take ownership of the values of any variables it uses from the environment.For example:
Rustuse std::thread;fn main() { let message = String::from("Hello from main!"); thread::spawn(move |
-
| {
println!("{}", message);
});
// println!("{}", message); // This would cause a compile error as 'message' has been moved
}
```
In this case, the `move` keyword ensures that the `message` `String` is moved into the closure that the new thread will execute. Consequently, the `message` variable is no longer valid in the main thread after the `spawn` call due to Rust's single ownership rule [7, 10, 11]. * **Sharing immutable data:** Immutable data can be shared freely and safely between threads in Rust. This is because Rust's borrowing rules allow for multiple immutable references to the same data at any given time [[44], S_S65, [61]]. Since immutable data cannot be changed, there is no risk of data races when multiple threads access it concurrently. * **Sharing mutable data with `Arc` and `Mutex`:** Sharing mutable data across threads requires careful synchronization to prevent data races. Rust provides the `std::sync::Arc` (Atomically Reference Counted) smart pointer and the `std::sync::Mutex` (Mutual Exclusion) primitive for this purpose [1, 12, 13, 14, 15]. `Arc` allows multiple threads to have shared, immutable ownership of a value. The `Mutex` then provides a mechanism to ensure that only one thread can access and modify the data it protects at any given time.Consider the following example: ```rust use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec!; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move |
| {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Counter: {}", *counter.lock().unwrap());
}
```
Here, `Arc::new(Mutex::new(0))` creates a mutex-protected counter wrapped in an `Arc`. Each spawned thread gets a clone of the `Arc`, which increments the reference count, allowing multiple threads to own the `Mutex`. Inside each thread, `counter.lock().unwrap()` attempts to acquire the lock on the mutex. If successful, it returns a `MutexGuard` that provides mutable access to the inner `i32`. The `unwrap()` call handles the `Result` returned by `lock()`, which could be an error if the mutex was poisoned due to a panic in another thread [1, 12, 13, 15]. The mutex is automatically unlocked when the `MutexGuard` goes out of scope.
Sharing mutable data withArcandRwLock: In scenarios where shared data is read much more frequently than it is written,std::sync::RwLock(Read-Write Lock) can offer better performance compared toMutex.RwLockallows multiple threads to hold a read lock simultaneously, but only one thread can hold a write lock at any given time, providing exclusive access for modification [1, 11, 13, 16, 17, 18, 19].
- 2.3 Waiting for Thread Completion (
JoinHandle) Thestd::thread::spawn()function returns astd::thread::JoinHandle3. ThisJoinHandleis an owned value that, when itsjoin()method is called, will block the current thread until the thread associated with theJoinHandlefinishes its execution 3. Thejoin()method returns aResult<T, E>, whereTis the type of the value returned by the spawned thread's closure (if any), andEis an error type that can occur if the spawned thread panics 5. Using.unwrap()on the result is a common way to handle successful completion and propagate panics. If aJoinHandleis dropped without callingjoin()on it, the spawned thread will continue to run independently, and the main thread will not wait for it to complete, effectively becoming a detached thread 5.
3. Message Passing with Channels
-
3.1 Creating Channels (mpsc::channel, sync_channel)
Rust's standard library provides channels for facilitating message-based communication between threads through the std::sync::mpsc module 5. The acronym mpsc stands for Multi-Producer, Single-Consumer, which describes the fundamental nature of these channels 10.
The
mpsc::channel()function is used to create an asynchronous, unbounded channel. This function returns a tuple containing aSender<T>and aReceiver<T>5. Sends on an asynchronous channel are non-blocking, meaning the sending thread will not wait for the message to be received.Alternatively,
mpsc::sync_channel(bound: usize)creates a synchronous, bounded channel with an internal buffer of a fixed size specified by theboundargument 25. When the buffer of a synchronous channel is full, any subsequent send operations will block the calling thread until space becomes available in the buffer. Aboundof 0 results in a "rendezvous channel," where a send operation will block until a corresponding receive operation is initiated on the other end of the channel 25.The type parameter
TinSender<T>andReceiver<T>indicates the type of the messages that can be transmitted through the channel 24. -
3.2 Senders and Receivers (Sender, Receiver)
The Sender<T> is the transmitting end of the channel and is used to send messages 24. A crucial feature of Sender is that it can be cloned using the clone() method. This allows multiple threads to obtain their own Sender and send messages to the same Receiver, thus enabling the multi-producer aspect of mpsc channels 24. It is important to note that for a Receiver to know when no more messages will be sent (especially when iterating over the receiver), all Sender instances associated with that Receiver, including the original, must be dropped 37.
The
Receiver<T>is the receiving end of the channel and is responsible for retrieving messages that have been sent 24. AReceiverhas the restriction of single ownership, meaning that only one thread can own a particularReceiverinstance at any given time 25. -
3.3 Sending and Receiving Messages (send, recv, try_recv)
To send a message through a channel, the send() method of the Sender is used. This method takes the message as an argument and returns a Result<(), SendError<T>> 5. An Ok(()) result indicates that the message was successfully sent, while an Err(SendError<T>) result signifies that the receiving end of the channel has been dropped, and the message could not be delivered. Notably, the send() method takes ownership of the message being sent 10.
To receive a message from the channel, the
recv()method of theReceiveris called. This method will block the current thread until a message becomes available on the channel. It returns aResult<T, RecvError>5. AnOk(message)result contains the received message, and anErr(RecvError)result indicates that allSenders associated with this channel have been dropped, and no more messages will ever be received.The
try_recv()method provides a non-blocking way to attempt to receive a message. It returns aResult<T, TryRecvError>34. AnOk(message)is returned if a message was immediately available. AnErr(TryRecvError::Empty)is returned if no message was available at the time of the call. AnErr(TryRecvError::Disconnected)is returned if allSenders for the channel have been dropped.Additionally, a
Receivercan be used as a blocking iterator in aforloop (for msg in rx). This loop will continue to yield messages as they arrive on the channel until the channel is closed (i.e., all associated senders are dropped) 36.
4. Shared State Concurrency
-
4.1 Mutual Exclusion with std::sync::Mutex
The std::sync::Mutex<T> is a fundamental synchronization primitive in Rust that provides mutual exclusion, ensuring that only one thread can access the protected data T at any given time. This mechanism is crucial for preventing data races when multiple threads need to read and write to shared resources 1.
A
Mutexis created using theMutex::new(value)constructor, wherevalueis the initial data to be protected 15.Acquiring and Releasing Locks: To access the data protected by a
Mutex, a thread must first acquire the lock. This is done by calling thelock()method on theMutex. Thelock()method will block the current thread until the mutex is free, at which point it acquires the lock and returns aResult<MutexGuard<T>, PoisonError<()>>15. TheMutexGuard<T>is an RAII (Resource Acquisition Is Initialization) guard. It provides exclusive access to the underlying data through theDeref(for immutable access) andDerefMut(for mutable access) traits. Importantly, theMutexGuardautomatically releases the lock when it goes out of scope, ensuring that the mutex is not held indefinitely, even if a panic occurs 44.The
try_lock()method offers a non-blocking way to attempt to acquire the lock. It returns aResult<MutexGuard<T>, TryLockError>, whereOkcontains the guard if the lock was acquired immediately, andErrindicates that the lock is currently held by another thread 15.Poisoning: A
Mutexin Rust has a safety feature called poisoning. If a thread that is holding aMutexpanics, theMutexis considered poisoned. Subsequent attempts by other threads to acquire the lock usinglock()will return anErrcontaining aPoisonError. This mechanism alerts other threads that the data protected by the mutex might be in an inconsistent or corrupted state due to the panic. However, even a poisonedMutexdoes not prevent all access to the underlying data. ThePoisonErrorhas aninto_inner()method that can be called to obtain aMutexGuard, allowing access to the potentially tainted data for inspection or recovery 44. -
4.2 Read-Write Locks with std::sync::RwLock
The std::sync::RwLock<T> is another synchronization primitive that manages access to shared data T. Unlike Mutex, which provides exclusive access to a single thread, RwLock allows for more flexible access patterns by permitting either multiple readers to access the data concurrently or a single writer to access it exclusively 1. This makes RwLock particularly useful in scenarios where reads are much more frequent than writes, as it can improve performance by allowing multiple readers to proceed without blocking each other.
An
RwLockis created using theRwLock::new(value)constructor 16.Read Access: To acquire a read lock, the
read()method is called. This method will block the current thread until there are no writers holding the lock, at which point it returns aResult<RwLockReadGuard<T>, PoisonError<()>>16. TheRwLockReadGuardprovides shared, read-only access to the underlying data through theDereftrait. Multiple threads can hold read locks simultaneously. Thetry_read()method attempts to acquire a read lock without blocking 16.Write Access: To acquire an exclusive write lock, the
write()method is used. This method will block the current thread until there are no readers or writers currently holding the lock, ensuring exclusive access for the writer. It returns aResult<RwLockWriteGuard<T>, PoisonError<()>>16. TheRwLockWriteGuardprovides mutable access to the data through theDerefMuttrait. Only one thread can hold a write lock at any time. Thetry_write()method attempts to acquire a write lock without blocking 16.Poisoning: Similar to
Mutex, anRwLockcan become poisoned. However, poisoning inRwLockonly occurs if a thread panics while holding an exclusive write lock. If a panic happens while a read lock is held, theRwLockis not poisoned 16.
| Feature | Mutex | RwLock |
| Access | Exclusive | Shared (read) or Exclusive (write) |
| Concurrency | Single thread | Multiple readers or single writer |
| Use Cases | General shared mutable data | Read-heavy shared mutable data |
| Poisoning | On panic while holding lock | On panic while holding write lock |
5. Asynchronous Programming with async and await
-
5.1 Defining Asynchronous Functions (async fn)
The async keyword, when placed before the fn keyword in Rust, defines an asynchronous function 9. Unlike regular synchronous functions, an async fn does not execute immediately. Instead, it returns a Future, which represents a value that will be produced at some point in the future 45. This mechanism enables non-blocking operations, allowing the program to continue executing other tasks while waiting for the result of the asynchronous operation 45.
-
5.2 Using the .await Keyword
The .await keyword is used inside an async function or an async block to pause the execution of that function or block until the Future it is applied to completes 9. When .await is encountered, the current asynchronous task yields control of the thread to the executor, allowing other asynchronous tasks to make progress. This is a key aspect of non-blocking I/O and other asynchronous operations. Once the awaited Future completes and produces its output, the execution of the async function or block resumes from the point immediately after the .await call 45.
-
5.3 async Blocks vs. async fn
While both async fn and async blocks are used to create Futures, they serve slightly different purposes 9. An async fn defines a named asynchronous function that can be called and its returned Future can be awaited. On the other hand, async blocks are anonymous expressions that evaluate to a Future. They are often used within synchronous functions to create a Future that can then be returned, or within other asynchronous contexts for grouping a sequence of operations into a single Future. Both async fn and async blocks can use the move keyword to transfer ownership of captured variables into the Future, allowing the Future to outlive the scope where the variables were originally defined.
| Feature | async fn | async Block |
| Definition | Asynchronous function | Expression evaluating to a Future |
| Usage | Defines an asynchronous operation | Creates a Future within a function or scope |
| Return Type | Returns a Future | Evaluates to a Future |
| Execution | Lazy; returns a Future to be .awaited | Lazy; the block's code runs when the Future is .awaited |
6. Futures and Executors
-
6.1 Understanding the Future Trait and poll Method
The std::future::Future trait is at the heart of asynchronous programming in Rust. It represents the eventual result of an asynchronous computation 45. The trait defines an associated type Output, which specifies the type of the value that the Future will produce upon completion 45. The core of the Future trait is the poll method, with the signature fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> 45. The self: Pin<&mut Self> argument is a pinned mutable reference to the Future, ensuring that the future's memory location remains stable. The cx: &mut Context argument provides access to a Waker, which is a mechanism for the Future to signal the executor that it is ready to make progress. The poll method returns a Poll<Self::Output> enum, which can be either Poll::Ready(output) if the future has completed and produced its output, or Poll::Pending if the future is not yet complete. If Poll::Pending is returned, the Future must ensure that the Waker is signaled when it is able to make further progress 45.
-
6.2 Introduction to Executors (tokio, async-std)
Executors are responsible for taking Futures and driving them to completion by repeatedly calling their poll methods until they return Poll::Ready 45. In the Rust ecosystem, tokio and async-std are two of the most popular asynchronous runtimes (executors) 9. tokio is a comprehensive, production-ready runtime that provides an event loop, task scheduler, asynchronous I/O primitives, timers, and many other utilities necessary for building scalable network applications 9. To use Tokio, the #[tokio::main] attribute is typically applied to the main function, which sets up the Tokio runtime 46. async-std is another asynchronous runtime that aims to provide an asynchronous version of the Rust standard library, offering a similar set of functionalities to Tokio 9. It also provides a #[async_std::main] attribute for setting up its runtime 52.
-
6.3 Basic Usage of Executors
To begin using asynchronous programming with an executor, the entry point of the application, typically the main function, is annotated with the executor's main attribute: #[tokio::main] for Tokio or #[async_std::main] for async-std 46. To run asynchronous tasks concurrently, the executor's spawning function is used: tokio::spawn() in Tokio or async_std::task::spawn() in async-std 49. The .await keyword is then used within async functions or blocks to wait for the completion of a spawned task or another Future 45.
7. Common Concurrency Patterns
-
7.1 Producer-Consumer Pattern (using channels)
The producer-consumer pattern is a classic concurrency pattern that involves one or more producer threads generating data and sending it through a channel (std::sync::mpsc) to one or more consumer threads that receive and process the data 36. Producers use the sender.send() method to enqueue data into the channel, while consumers use the receiver.recv() method or iterate over the receiver to dequeue and process the data 36. This pattern is highly effective for decoupling the production of data from its consumption, allowing different parts of a concurrent program to operate independently and efficiently. For instance, one thread might be responsible for reading data from a sensor (producer) and sending it through a channel to another thread that performs analysis on the data (consumer).
-
7.2 Worker Pools (overview and potential implementation strategies)
A worker pool is a concurrency pattern where a set of worker threads is created and waits for tasks to be assigned to them from a shared queue. This queue is often implemented using a channel 1. A central dispatcher thread, or potentially multiple producer threads, add tasks to this queue. The worker threads then continuously take tasks from the queue and execute them, potentially sending the results back through another channel if necessary 1. Libraries such as rayon and tokio provide high-level abstractions for creating and managing thread pools, often with features like work stealing to improve efficiency 1. A basic implementation of a worker pool might involve using an Arc<Mutex<Vec<Task>>> to hold the queue of tasks and a channel to signal worker threads when a new task is available.
-
7.3 Atomic Operations (std::sync::atomic)
The std::sync::atomic module in Rust provides a way to perform simple operations on shared data without the need for explicit locks. This can often lead to better performance in scenarios with high contention on simple data types like counters or flags 1. Operations such as fetch_add, fetch_sub, load, store, and compare_and_swap are atomic, meaning they are guaranteed to complete in a single, indivisible step, even when accessed by multiple threads concurrently 56. Using atomic operations correctly often requires careful consideration of memory ordering, specified through the Ordering enum (Ordering::Relaxed, Ordering::Acquire, Ordering::Release, Ordering::AcquireRelease, Ordering::SeqCst), to ensure proper synchronization and visibility of changes across threads 1.
| Atomic Type | Description |
AtomicBool | Atomic boolean value |
AtomicIsize | Atomic signed integer whose size depends on the architecture |
AtomicUsize | Atomic unsigned integer whose size depends on the architecture |
AtomicI8 | Atomic 8-bit signed integer |
AtomicU8 | Atomic 8-bit unsigned integer |
AtomicI32 | Atomic 32-bit signed integer |
AtomicU32 | Atomic 32-bit unsigned integer |
AtomicPtr<T> | Atomic raw pointer |
8. Send and Sync Traits
-
8.1 Send: Transferring Ownership Between Threads
In Rust's concurrency model, the Send marker trait plays a crucial role in ensuring that types can be safely moved from one thread to another 1. A type T is Send if it is safe to transfer ownership of a value of type T across thread boundaries. This is essential to prevent data races, as it guarantees that if a piece of data is owned by one thread, no other thread can simultaneously have mutable access to it unless proper synchronization mechanisms are in place. Most primitive types in Rust, as well as types composed entirely of Send types, are automatically Send 42. However, certain types, such as raw pointers (*const T, *mut T), UnsafeCell, Rc, and MutexGuard, are notable exceptions and are not Send 42.
-
8.2 Sync: Safe Shared Access Across Threads
The Sync marker trait indicates that a type T is safe to be shared between multiple threads through immutable references (&T) 1. In other words, if a shared reference (&T) to a type T is Send, then T is Sync. This trait ensures that concurrent, immutable access to data will not lead to undefined behavior. Similar to Send, most primitive types and types composed solely of Sync types are automatically Sync 42. Smart pointers like Arc<T>, Mutex<T>, and RwLock<T> (provided that the inner type T is also Send and Sync) are Sync, as they manage concurrent access internally 42. Conversely, types like Rc, Cell, and RefCell are not Sync due to their mechanisms for providing non-thread-safe shared mutability or unsynchronized reference counting 42.
-
8.3 Examples of Send and Sync Types
Common types in Rust that are both Send and Sync include fundamental types like i32, bool, char, and f64, as well as immutable string slices (&'static str). Furthermore, many standard library collections such as Vec<i32>, String, and HashMap<String, i32> (assuming their type parameters also satisfy Send and Sync) are also Send and Sync. Smart pointers designed for concurrent use, like Arc<Mutex<i32>> and Arc<RwLock<String>>, also implement both traits.
-
8.4 Types That Are Not Send or Sync
Several types in Rust are either not Send, not Sync, or neither. These include raw pointers (*const T, *mut T), the interior mutability primitives UnsafeCell<T>, Cell<T>, and RefCell<T>, and the single-threaded reference-counted pointer Rc<T>. Additionally, types that contain raw pointers without proper encapsulation to ensure thread safety, thread-local variables (as their values are specific to each thread), and handles to operating system or hardware resources that are tied to a particular thread are typically not Send or Sync.
9. Best Practices and Common Pitfalls
-
9.1 Avoiding Data Races
To prevent data races in concurrent Rust code, it is essential to leverage the language's ownership and borrowing rules to strictly control how shared data is accessed across threads 1. When mutable state needs to be shared, synchronization primitives such as Mutex and RwLock should be employed to ensure that only one thread (or multiple readers in the case of RwLock) can access the data at any given time 1. It is also crucial to ensure that any types being passed to new threads satisfy the Send trait, and any types being accessed from multiple threads satisfy the Sync trait 1. Whenever possible, favoring the sharing of immutable data can significantly reduce the complexity and potential for errors in concurrent programs 1.
-
9.2 Preventing Deadlocks
Deadlocks, a common issue in concurrent programming, can occur when two or more threads are blocked indefinitely, waiting for each other to release resources. To minimize the risk of deadlocks in Rust, it is important to establish a consistent order in which multiple locks are acquired 1. Holding multiple locks simultaneously for extended periods should be avoided, especially if the code within the locked sections might perform I/O or call other potentially blocking operations 1. Using non-blocking lock acquisition methods like try_lock or setting timeouts on lock acquisition attempts can help in detecting and potentially recovering from deadlock situations 17. When working with asynchronous code using async/.await, it is important to be mindful of interactions with traditional blocking synchronization primitives and to avoid holding locks across .await points, as this can lead to unexpected blocking of the asynchronous runtime 45.
-
9.3 Choosing the Right Concurrency Primitives
Selecting the appropriate concurrency primitives for a given task is crucial for both the correctness and performance of a Rust application. For CPU-bound tasks that can benefit from parallel execution, using std::thread to spawn new threads is often a suitable approach 1. When communication between threads is required, especially in producer-consumer scenarios, channels provided by std::sync::mpsc are an excellent choice 1. For protecting shared mutable data where exclusive access is needed, Mutex is the standard primitive 1. In read-heavy scenarios involving shared mutable data, RwLock can offer performance benefits by allowing concurrent readers 1. For simple, lock-free synchronization of primitive types, the atomic operations in std::sync::atomic can be very efficient 1. Finally, for I/O-bound operations, using async/.await in conjunction with an asynchronous runtime like tokio or async-std allows for non-blocking execution, improving the application's responsiveness 1.
-
9.4 Minimizing Lock Contention
High lock contention can significantly degrade the performance of concurrent applications by causing threads to spend excessive time waiting to acquire locks. To minimize lock contention, it is best practice to keep critical sections of code (the parts within lock guards) as short as possible 1. Performing expensive or potentially blocking operations while holding a lock should be avoided 1. In some cases, using finer-grained locking, where a single lock protecting a large data structure is broken down into multiple smaller locks protecting different parts of the data, can help to reduce contention 43. For highly contended scenarios, exploring lock-free data structures, which often rely on atomic operations, might be beneficial, although these are generally more complex to implement correctly.
-
9.5 Importance of Send and Sync Bounds
When working with threads in Rust, it is crucial to pay close attention to the Send and Sync traits 1. The Rust compiler enforces these trait bounds to ensure that data is safely shared or moved between threads. Always ensure that types passed to threads or shared between threads satisfy these traits as required by the concurrency primitives being used. Be particularly aware of types that do not implement Send or Sync and handle them appropriately, either by keeping them confined to a single thread or by using suitable synchronization mechanisms if sharing is necessary 42. The compiler's role in checking these bounds is invaluable in preventing many common concurrency errors.
