Send and Sync

This isn’t a thorough tutorial of the Send and Sync traits, rather my notes to help me understand it as best I can. See the Rust book, the Rustonomicon for more details, or check the sources list at the bottom for more interesting articles. This also means that there may be inaccuracies or downright falsehoods here! None intentional, but please let me know if there are any.

Send

A type is Send if it is safe to send it to another thread. “Sending” means transferring ownership, or moving the variable to the new thread.

// This program prints:
// [src/main.rs:6] x = 8

fn () -> <(), <dyn std::error::>> {
    let  = 8;
    std::thread::(
        // The `move` keyword transfers ownership of `x`
        move || !(),
    )
    .()
    .();

    (())
}

Most types are Send. Additionally, any type that only consists of Send types are Send as well.

Sync

T is Sync if and only if &T is Send.

A type is Sync if it is safe to share it with another thread. “Sharing” means allowing multiple threads to reference the same variable, which can only happen if a reference to that variable can be sent to the other thread.

// This program prints:
// [src/main.rs:4] &x = 8

fn () -> <(), <dyn std::error::>> {
    let  = 8;
    std::thread::(|| {
        .(|| !(&));
    });

    (())
}

In the above example, since x is Sync, &x is Send, and we can reference x from another thread.

Always Not Send and Not Sync

Here are a couple types that are !Send and !Sync by default:

  • Raw pointers (*const T, *mut T)

    These are not Send or Sync by default because it is up to the programmer to ensure proper usage of the pointers, as they are commonly used in FFI. Unlike references, the compiler does not track the lifetime of pointers.

  • Reference-counting pointers (Rc<T>)

    This type allows a single thread to hold multiple Rc<T> to a shared T. The contained T and any reference counts are stored in a shared memory location to allow shared access from multiple Rc<T>.

    Since the shared memory location is accessed without synchronization, Rc<T> cannot be Send to prevent another thread from causing a data race.

    Similarly, Rc<T> cannot be Sync, because cloning an Rc<T> can be done with a shared reference, which implies accessing the underlying reference count.

Sometimes Send but Not Sync

The Cell<T> type is Send if T is Send. It is !Sync because it allows interior mutability, which allows retrieving, setting, or swapping T given a &Cell<T>. This access is unsynchronized, which can cause a data race if done concurrently from multiple threads.

For similar reasons, the RefCell<T> type is Send if T: Send but !Sync.

Sometimes Sync but Not Send

The MutexGuard<T> type is

  • Sync if T: Sync

    This wasn’t always the case! There used to be a bug where MutexGuard<T> was Sync if T is Send, since

    1. Sync is an auto-trait
    2. MutexGuard<T> only contains a &Mutex<T> and a poison::Guard, not a T directly
    3. &Mutex<T>: Sync if Mutex<T>: Sync (which requires T: Send), and poison::Guard: Sync

    The danger with having MutexGuard<T> be Sync if T: Send is the potential for data races if T were a type with interior mutability like Cell<U>, which is Send (if U: Send) but !Sync.

  • !Send

    A mutex must always be unlocked on the same thread that locked it. Since the mutex is unlocked by dropping its guard, the guard cannot be sent to another thread.

Sometimes Send or Sync

Some wrapper types are Send or Sync if and only if their inner type are Send or Sync respectively. An example of that is Option<T>.

However, some types have notably different constraints. The Mutex<T> type is Send if T is Send, but Sync if T is Send.

This was slightly confusing at first. Here’s a hopefully lucid proof by counterexample:

  1. Assume Mutex<T> can be Sync when T is !Send.
  2. Since Mutex<T> is Sync, Thread A can share &Mutex<T> with Thread B.
  3. Thread B can then call Mutex::lock(&self), returning a MutexGuard<T>.
  4. Thread B can then call MutexGuard::deref_mut(&mut self), returning &mut T. So far so good, the mutex is still used as it normally is.
  5. We can take ownership through mutable references. For example, if T is Option<U> where U is !Send (implying T is !Send), Option::take(&mut self) can return Some(U), thereby sending U from Thread A to Thread B.
  6. Therefore, Mutex<T> cannot be Sync if T is !Send.

Why does T not have to be Sync then? It is because getting access to &mut T (hence &T or T) requires locking the mutex, which ensures that only one thread access at a time! That’s the whole point of a Mutex, to allow sharing unshareable types.

The Arc<T> type is also a bit confusing. It is only Send or Sync if T: Send + Sync.

  • For Arc<T>: Send
    • T: Send is required because moving Arc<T> to another thread moves T along with it.

      T: Sync is required because Arc<T>::deref(&self) returns a &T.

  • For Arc<T>: Sync
    • T: Send is required because you can Arc<T>::clone(&self), then keep the cloned Arc around until it is the last strong reference then take T out with try_unwrap or drop T by dropping the cloned Arc.

      T: Sync is required because Arc<T>::deref(&self) returns a &T.

Shared References

For immutable references, &T: Send requires T: Sync. This is the definition of Sync, which is not surprising. Additionally, &T: Sync if and only if T: Sync. This is because if &T: Sync when T: !Sync, we can send &&Cell<U> to another thread (by definition of Sync), then the Deref trait allows getting a &Cell<U>, which is invalid.

Exclusive References

Mutable references &mut T: Send require T: Send. This has a straightforward reason: it’s easy to transfer ownership of a T to another thread if you have mutable access, for example with Option::take(&mut self). Additionally, &mut T: Sync if and only if T: Sync. While this might seem like you can share mutable references across threads, in actuality it means that you can send shared references to mutable references (& &mut T), which act as immutable references.

Conclusion

Writing this helped me understand Send and Sync more, so I consider that a win!

Sources

alloc::boxed
pub struct Box<T, A = Global>(Unique<T>, A)
where
    T: ?Sized,
    A: Allocator,

A pointer type that uniquely owns a heap allocation of type T.

See the module-level documentation for more.

std::thread
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,

Spawns a new thread, returning a JoinHandle for it.

The join handle provides a [join] method that can be used to join the spawned thread. If the spawned thread panics, [join] will return an [Err] containing the argument given to panic.

If the join handle is dropped, the spawned thread will implicitly be detached. In this case, the spawned thread may no longer be joined. (It is the responsibility of the program to either eventually join threads it creates or detach them; otherwise, a resource leak will result.)

This call will create a thread using default parameters of Builder, if you want to specify the stack size or the name of the thread, use this API instead.

As you can see in the signature of spawn there are two constraints on both the closure given to spawn and its return value, let’s explain them:

  • The 'static constraint means that the closure and its return value must have a lifetime of the whole program execution. The reason for this is that threads can outlive the lifetime they have been created in.

    Indeed if the thread, and by extension its return value, can outlive their caller, we need to make sure that they will be valid afterwards, and since we can’t know when it will return we need to have them valid as long as possible, that is until the end of the program, hence the 'static lifetime.

  • The Send constraint is because the closure will need to be passed by value from the thread where it is spawned to the new thread. Its return value will need to be passed from the new thread to the thread where it is joined. As a reminder, the Send marker trait expresses that it is safe to be passed from thread to thread. Sync expresses that it is safe to have a reference be passed from thread to thread.

Panics

Panics if the OS fails to create a thread; use Builder::spawn to recover from such errors.

Examples

Creating a thread.

use std::thread;

let handler = thread::spawn(|| {
    // thread code
});

handler.join().unwrap();

As mentioned in the module documentation, threads are usually made to communicate using [channels], here is how it usually looks.

This example also shows how to use move, in order to give ownership of values to a thread.

use std::thread;
use std::sync::mpsc::channel;

let (tx, rx) = channel();

let sender = thread::spawn(move || {
    tx.send("Hello, thread".to_owned())
        .expect("Unable to send on channel");
});

let receiver = thread::spawn(move || {
    let value = rx.recv().expect("Unable to receive from channel");
    println!("{value}");
});

sender.join().expect("The sender thread has panicked");
receiver.join().expect("The receiver thread has panicked");

A thread can also return a value through its JoinHandle, you can use this to make asynchronous computations (futures might be more appropriate though).

use std::thread;

let computation = thread::spawn(|| {
    // Some expensive computation.
    42
});

let result = computation.join().unwrap();
println!("{result}");

Notes

This function has the same minimal guarantee regarding “foreign” unwinding operations (e.g. an exception thrown from C++ code, or a panic! in Rust code compiled or linked with a different runtime) as catch_unwind; namely, if the thread created with thread::spawn unwinds all the way to the root with such an exception, one of two behaviors are possible, and it is unspecified which will occur:

  • The process aborts.
  • The process does not abort, and [join] will return a Result::Err containing an opaque type.
core::result::Result
impl<T, E> Result<T, E>
pub fn unwrap(self) -> T
where
    E: fmt::Debug,

Returns the contained Ok value, consuming the self value.

Because this function may panic, its use is generally discouraged. Panics are meant for unrecoverable errors, and may abort the entire program.

Instead, prefer to use the ? (try) operator, or pattern matching to handle the Err case explicitly, or call [unwrap_or], [unwrap_or_else], or [unwrap_or_default].

Panics

Panics if the value is an Err, with a panic message provided by the Err’s value.

Examples

Basic usage:

let x: Result<u32, &str> = Ok(2);
assert_eq!(x.unwrap(), 2);
let x: Result<u32, &str> = Err("emergency failure");
x.unwrap(); // panics with `emergency failure`
core::result
pub enum Result<T, E> {
    Ok( /* … */ ),
    Err( /* … */ ),
}

Result is a type that represents either success (Ok) or failure (Err).

See the documentation for details.

core::error
pub trait Error
where
    Self: Debug + Display,

Error is a trait representing the basic expectations for error values, i.e., values of type E in Result<T, E>.

Errors must describe themselves through the Display and Debug traits. Error messages are typically concise lowercase sentences without trailing punctuation:

let err = "NaN".parse::<u32>().unwrap_err();
assert_eq!(err.to_string(), "invalid digit found in string");

Errors may provide cause information. Error::source is generally used when errors cross “abstraction boundaries”. If one module must report an error that is caused by an error from a lower-level module, it can allow accessing that error via Error::source. This makes it possible for the high-level module to provide its own errors while also revealing some of the implementation for debugging.

core::result::Result
Ok(T)

Contains the success value

std::thread::scoped::Scope
impl<'scope, 'env> Scope<'scope, 'env>
pub fn spawn<F, T>(&'scope self, f: F) -> ScopedJoinHandle<'scope, T>
where
    F: FnOnce() -> T + Send + 'scope,
    T: Send + 'scope,

Spawns a new thread within a scope, returning a ScopedJoinHandle for it.

Unlike non-scoped threads, threads spawned with this function may borrow non-'static data from the outside the scope. See scope for details.

The join handle provides a [join] method that can be used to join the spawned thread. If the spawned thread panics, [join] will return an Err containing the panic payload.

If the join handle is dropped, the spawned thread will be implicitly joined at the end of the scope. In that case, if the spawned thread panics, scope will panic after all threads are joined.

This call will create a thread using default parameters of Builder. If you want to specify the stack size or the name of the thread, use Builder::spawn_scoped instead.

Panics

Panics if the OS fails to create a thread; use Builder::spawn_scoped to recover from such errors.

std::thread::JoinHandle
impl<T> JoinHandle<T>
pub fn join(self) -> Result<T>

Waits for the associated thread to finish.

This function will return immediately if the associated thread has already finished.

In terms of [atomic memory orderings], the completion of the associated thread synchronizes with this function returning. In other words, all operations performed by that thread happen before all operations that happen after join returns.

If the associated thread panics, [Err] is returned with the parameter given to panic (though see the Notes below).

Panics

This function may panic on some platforms if a thread attempts to join itself or otherwise may create a deadlock with joining threads.

Examples

use std::thread;

let builder = thread::Builder::new();

let join_handle: thread::JoinHandle<_> = builder.spawn(|| {
    // some work here
}).unwrap();
join_handle.join().expect("Couldn't join on the associated thread");

Notes

If a “foreign” unwinding operation (e.g. an exception thrown from C++ code, or a panic! in Rust code compiled or linked with a different runtime) unwinds all the way to the thread root, the process may be aborted; see the Notes on [thread::spawn]. If the process is not aborted, this function will return a Result::Err containing an opaque type.

src
fn main() -> Result<(), Box<dyn std::error::Error>>
let x: i32
scope: &Scope<'_, '_>
std::macros
macro_rules! dbg

Prints and returns the value of a given expression for quick and dirty debugging.

An example:

let a = 2;
let b = dbg!(a * 2) + 1;
//      ^-- prints: [src/main.rs:2:9] a * 2 = 4
assert_eq!(b, 5);

The macro works by using the Debug implementation of the type of the given expression to print the value to stderr along with the source location of the macro invocation as well as the source code of the expression.

Invoking the macro on an expression moves and takes ownership of it before returning the evaluated expression unchanged. If the type of the expression does not implement Copy and you don’t want to give up ownership, you can instead borrow with dbg!(&expr) for some expression expr.

The dbg! macro works exactly the same in release builds. This is useful when debugging issues that only occur in release builds or when debugging in release mode is significantly faster.

Note that the macro is intended as a debugging tool and therefore you should avoid having uses of it in version control for long periods (other than in tests and similar). Debug output from production code is better done with other facilities such as the debug! macro from the log crate.

Stability

The exact output printed by this macro should not be relied upon and is subject to future changes.

Panics

Panics if writing to io::stderr fails.

Further examples

With a method call:

fn foo(n: usize) {
    if let Some(_) = dbg!(n.checked_sub(4)) {
        // ...
    }
}

foo(3)

This prints to stderr:

[src/main.rs:2:22] n.checked_sub(4) = None

Naive factorial implementation:

fn factorial(n: u32) -> u32 {
    if dbg!(n <= 1) {
        dbg!(1)
    } else {
        dbg!(n * factorial(n - 1))
    }
}

dbg!(factorial(4));

This prints to stderr:

[src/main.rs:2:8] n <= 1 = false
[src/main.rs:2:8] n <= 1 = false
[src/main.rs:2:8] n <= 1 = false
[src/main.rs:2:8] n <= 1 = true
[src/main.rs:3:9] 1 = 1
[src/main.rs:7:9] n * factorial(n - 1) = 2
[src/main.rs:7:9] n * factorial(n - 1) = 6
[src/main.rs:7:9] n * factorial(n - 1) = 24
[src/main.rs:9:1] factorial(4) = 24

The dbg!(..) macro moves the input:

/// A wrapper around `usize` which importantly is not Copyable.
#[derive(Debug)]
struct NoCopy(usize);

let a = NoCopy(42);
let _ = dbg!(a); // <-- `a` is moved here.
let _ = dbg!(a); // <-- `a` is moved again; error!

You can also use dbg!() without a value to just print the file and line whenever it’s reached.

Finally, if you want to dbg!(..) multiple values, it will treat them as a tuple (and return it, too):

assert_eq!(dbg!(1usize, 2u32), (1, 2));

However, a single argument with a trailing comma will still not be treated as a tuple, following the convention of ignoring trailing commas in macro invocations. You can use a 1-tuple directly if you need one:

assert_eq!(1, dbg!(1u32,)); // trailing comma ignored
assert_eq!((1,), dbg!((1u32,))); // 1-tuple
std::thread::scoped
pub fn scope<'env, F, T>(f: F) -> T
where
    F: for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T,

Creates a scope for spawning scoped threads.

The function passed to scope will be provided a Scope object, through which scoped threads can be spawned.

Unlike non-scoped threads, scoped threads can borrow non-'static data, as the scope guarantees all threads will be joined at the end of the scope.

All threads spawned within the scope that haven’t been manually joined will be automatically joined before this function returns.

Panics

If any of the automatically joined threads panicked, this function will panic.

If you want to handle panics from spawned threads, join them before the end of the scope.

Example

use std::thread;

let mut a = vec![1, 2, 3];
let mut x = 0;

thread::scope(|s| {
    s.spawn(|| {
        println!("hello from the first scoped thread");
        // We can borrow `a` here.
        dbg!(&a);
    });
    s.spawn(|| {
        println!("hello from the second scoped thread");
        // We can even mutably borrow `x` here,
        // because no other threads are using it.
        x += a[0] + a[2];
    });
    println!("hello from the main thread");
});

// After the scope, we can modify and access our variables again:
a.push(4);
assert_eq!(x, a.len());

Lifetimes

Scoped threads involve two lifetimes: 'scope and 'env.

The 'scope lifetime represents the lifetime of the scope itself. That is: the time during which new scoped threads may be spawned, and also the time during which they might still be running. Once this lifetime ends, all scoped threads are joined. This lifetime starts within the scope function, before f (the argument to scope) starts. It ends after f returns and all scoped threads have been joined, but before scope returns.

The 'env lifetime represents the lifetime of whatever is borrowed by the scoped threads. This lifetime must outlast the call to scope, and thus cannot be smaller than 'scope. It can be as small as the call to scope, meaning that anything that outlives this call, such as local variables defined right before the scope, can be borrowed by the scoped threads.

The 'env: 'scope bound is part of the definition of the Scope type.