In contrast to the technique of messaging in which we pass ownership of objects, the shared memory technique involves borrowing objects. With those two techniques we are thus back to the traditional Rust choice between ownership and borrowing.
In this chapter, we focus on sharing memory between threads. Thanks
to the Mutex structure, we will see how to share and access
individual objects between threads.
38.4.1Sync¶
An object can be shared between threads, if its type implements the Sync trait.
The
Synctrait is a marker trait, used by the compiler. We must never implement it manually for types.Most of built-in types and types from the standard library implement the
Synctrait. More precisely any typeTimplementsSyncif&TimplementsSend.Any type composed of types that implement
Sync, will also automatically implementSync.Among the types that do not implement
SyncareRc,RefCellandCell. Instead, one must useArcandMutex.
38.4.2Mutexes¶
A mutex (mutual exclusion) is a mechanism to control access to shared data. It uses an internal lock structure to keep track of which thread is currently using the data. To access the shared data controlled by the mutex, a thread must:
Acquire the lock.
Read/write into the shared data.
Release the lock so that other threads may access the data.
The Mutex structure implements the concept of the mutex. Some of its methods are presented in Table 1.
Table 1:Some methods of Mutex
Method | Description |
|---|---|
| |
| |
| |
| |
| |
| Creates a new mutex on the value |
|
38.4.3Locking¶
The Mutex structure provides a lock() method to acquire it. It
returns a
LockResult
that is just an alias for the Result enumerate with a special error
type
PoisonError.
The LockResult gives access to a
MutexGuard
around the data. The lock is automatically released when the
MutexGuard goes out of scope.
As an example we create a mutex on an integer value:
let m = std::sync::Mutex::new(13);
mCouldn't automatically determine type of variable `m`.
Please give it an explicit type.We call now the lock() method to acquire the lock on the mutex.
Since the lock() method returns a MutexGuard that implements RAII
(Resource acquisition is initialization), it ensures that the lock
will be released when getting out of scope. Hence we create a special
scope, using curly braces, to force the release of the lock. The
MutexGuard also implements Deref, which allows to access the
underlying data through the dereferencing operator * or by calling
its methods.
{
let mut n = m.lock().unwrap();
*n = 7; // Using the Deref trait we modify the underlying data
}After the release of the lock, we can check the mutex state and see that the value has changed:
m38.4.4Shared access¶
To share access to data between threads, we need to share a Mutex
between threads. Using an Rc pointer would not work, because it is
not thread-safe (i.e.: it does not handle concurrency access to its
interface). Instead we are going to use an Arc pointer. Arc stands
for Atomically Referenced Counted, and is just a thread-safe
version of Rc.
Here is an example extracted from The Rust Programming Language book (Shared Access to Mutex<T>), in which we launch 10 threads that each increment a shared counter by 1.
We start by importing the necessary objects:
use std::sync::{Arc, Mutex};
use std::thread;We create the counter inside a Mutex that will control its access,
and we place the mutex inside an Arc so we can share it across
threads:
let counter = Arc::new(Mutex::new(0));We initialize an empty vector that will store the thread handles:
let mut handles: Vec<std::thread::JoinHandle<()>> = Vec::new();We create 10 threads. For each thread:
We clone the
Arcobject so we can move the clone into the new thread.We spawn the new thread that will lock the mutex inside the
Arcobject and increment the counter. The lock is released when exiting the thread’s closure.We store the handle inside our vector.
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);
}We wait for each thread to terminate:
for handle in handles {
handle.join().unwrap();
}At the end, we check that the counter has been incremented:
println!("Result: {}", *counter.lock().unwrap());