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 ::::>> {
    let  = 8;
    ::::(
        // 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 ::::>> {
    let  = 8;
    ::::(|| {
        .(|| !(&));
    });

    (())
}

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

codeintel::block_69c341aeea319d7a
fn main() -> Result<(), Box<dyn std::error::Error>>
core::result
pub enum Result<T, E> {
    Ok( /* … */ ),
    Err( /* … */ ),
}

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

See the module documentation for details.

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.

extern crate std

The Rust Standard Library

The Rust Standard Library is the foundation of portable Rust software, a set of minimal and battle-tested shared abstractions for the broader Rust ecosystem. It offers core types, like [Vec<T>] and [Option<T>], library-defined operations on language primitives, standard macros, [I/O] and [multithreading], among many other things.

std is available to all Rust crates by default. Therefore, the standard library can be accessed in use statements through the path std, as in use std::env.

How to read this documentation

If you already know the name of what you are looking for, the fastest way to find it is to use the search button at the top of the page.

Otherwise, you may want to jump to one of these useful sections:

If this is your first time, the documentation for the standard library is written to be casually perused. Clicking on interesting things should generally lead you to interesting places. Still, there are important bits you don’t want to miss, so read on for a tour of the standard library and its documentation!

Once you are familiar with the contents of the standard library you may begin to find the verbosity of the prose distracting. At this stage in your development you may want to press the “ Summary” button near the top of the page to collapse it into a more skimmable view.

While you are looking at the top of the page, also notice the “Source” link. Rust’s API documentation comes with the source code and you are encouraged to read it. The standard library source is generally high quality and a peek behind the curtains is often enlightening.

What is in the standard library documentation?

First of all, The Rust Standard Library is divided into a number of focused modules, all listed further down this page. These modules are the bedrock upon which all of Rust is forged, and they have mighty names like [std::slice] and [std::cmp]. Modules’ documentation typically includes an overview of the module along with examples, and are a smart place to start familiarizing yourself with the library.

Second, implicit methods on primitive types are documented here. This can be a source of confusion for two reasons:

  1. While primitives are implemented by the compiler, the standard libraryimplements methods directly on the primitive types (and it is the onlylibrary that does so), which are documented in the section on primitives.
  2. The standard library exports many modules with the same name as primitive types. These define additional items related to the primitivetype, but not the all-important methods.

So for example there is a page for the primitive type char that lists all the methods that can be called on characters (very useful), and there is a page for the module std::char that documents iterator and error types created by these methods (rarely useful).

Note the documentation for the primitives [str] and [T] (also called ‘slice’). Many method calls on String and [Vec<T>] are actually calls to methods on [str] and [T] respectively, via deref coercions.

Third, the standard library defines [The Rust Prelude], a small collection of items - mostly traits - that are imported into every module of every crate. The traits in the prelude are pervasive, making the prelude documentation a good entry point to learning about the library.

And finally, the standard library exports a number of standard macros, and lists them on this page (technically, not all of the standard macros are defined by the standard library - some are defined by the compiler - but they are documented here the same). Like the prelude, the standard macros are imported by default into all crates.

Contributing changes to the documentation

Check out the Rust contribution guidelines here. The source for this documentation can be found on GitHub in the ‘library/std/’ directory. To contribute changes, make sure you read the guidelines first, then submit pull-requests for your suggested changes.

Contributions are appreciated! If you see a part of the docs that can be improved, submit a PR, or chat with us first on Zulip #docs.

A Tour of The Rust Standard Library

The rest of this crate documentation is dedicated to pointing out notable features of The Rust Standard Library.

Containers and collections

The option and result modules define optional and error-handling types, [Option<T>] and [Result<T, E>]. The iter module defines Rust’s iterator trait, Iterator, which works with the for loop to access collections.

The standard library exposes three common ways to deal with contiguous regions of memory:

  • [Vec<T>] - A heap-allocated vector that is resizable at runtime.
  • [T; N] - An inline array with a fixed size at compile time.
  • [T] - A dynamically sized slice into any other kind of contiguousstorage, whether heap-allocated or not.

Slices can only be handled through some kind of pointer, and as such come in many flavors such as:

  • &[T] - shared slice
  • &mut [T] - mutable slice
  • Box<[T]> - owned slice

[str], a UTF-8 string slice, is a primitive type, and the standard library defines many methods for it. Rust [str]s are typically accessed as immutable references: &str. Use the owned String for building and mutating strings.

For converting to strings use the format macro, and for converting from strings use the [FromStr] trait.

Data may be shared by placing it in a reference-counted box or the [Rc] type, and if further contained in a [Cell] or [RefCell], may be mutated as well as shared. Likewise, in a concurrent setting it is common to pair an atomically-reference-counted box, [Arc], with a [Mutex] to get the same effect.

The collections module defines maps, sets, linked lists and other typical collection types, including the common [HashMap<K, V>].

Platform abstractions and I/O

Besides basic data types, the standard library is largely concerned with abstracting over differences in common platforms, most notably Windows and Unix derivatives.

Common types of I/O, including [files], [TCP], and [UDP], are defined in the io, fs, and net modules.

The thread module contains Rust’s threading abstractions. sync contains further primitive shared memory types, including [atomic], [mpmc] and [mpsc], which contains the channel types for message passing.

Use before and after main()

Many parts of the standard library are expected to work before and after main(); but this is not guaranteed or ensured by tests. It is recommended that you write your own tests and run them on each platform you wish to support. This means that use of std before/after main, especially of features that interact with the OS or global state, is exempted from stability and portability guarantees and instead only provided on a best-effort basis. Nevertheless bug reports are appreciated.

On the other hand core and alloc are most likely to work in such environments with the caveat that any hookable behavior such as panics, oom handling or allocators will also depend on the compatibility of the hooks.

Some features may also behave differently outside main, e.g. stdio could become unbuffered, some panics might turn into aborts, backtraces might not get symbolicated or similar.

Non-exhaustive list of known limitations:

  • after-main use of thread-locals, which also affects additional features:
  • under UNIX, before main, file descriptors 0, 1, and 2 may be unchanged(they are guaranteed to be open during main,and are opened to /dev/null O_RDWR if they weren’t open on program start)
std
pub mod error
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");

Error source

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.

In error types that wrap an underlying error, the underlying error should be either returned by the outer error’s Error::source(), or rendered by the outer error’s Display implementation, but not both.

Example

Implementing the Error trait only requires that Debug and Display are implemented too.

use std::error::Error;
use std::fmt;
use std::path::PathBuf;

#[derive(Debug)]
struct ReadConfigError {
    path: PathBuf
}

impl fmt::Display for ReadConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let path = self.path.display();
        write!(f, "unable to read configuration at {path}")
    }
}

impl Error for ReadConfigError {}
let x: i32
std
pub mod thread

Native threads.

The threading model

An executing Rust program consists of a collection of native OS threads, each with their own stack and local state. Threads can be named, and provide some built-in support for low-level synchronization.

Communication between threads can be done through [channels], Rust’s message-passing types, along with other forms of thread synchronization and shared-memory data structures. In particular, types that are guaranteed to be threadsafe are easily shared between threads using the atomically-reference-counted container, Arc.

Fatal logic errors in Rust cause thread panic, during which a thread will unwind the stack, running destructors and freeing owned resources. While not meant as a ‘try/catch’ mechanism, panics in Rust can nonetheless be caught (unless compiling with panic=abort) with catch_unwind and recovered from, or alternatively be resumed with resume_unwind. If the panic is not caught the thread will exit, but the panic may optionally be detected from a different thread with [join]. If the main thread panics without the panic being caught, the application will exit with a non-zero exit code.

When the main thread of a Rust program terminates, the entire program shuts down, even if other threads are still running. However, this module provides convenient facilities for automatically waiting for the termination of a thread (i.e., join).

Spawning a thread

A new thread can be spawned using the thread::spawn function:

use std::thread;

thread::spawn(move || {
    // some work here
});

In this example, the spawned thread is “detached,” which means that there is no way for the program to learn when the spawned thread completes or otherwise terminates.

To learn when a thread completes, it is necessary to capture the JoinHandle object that is returned by the call to spawn, which provides a join method that allows the caller to wait for the completion of the spawned thread:

use std::thread;

let thread_join_handle = thread::spawn(move || {
    // some work here
});
// some work here
let res = thread_join_handle.join();

The [join] method returns a [thread::Result] containing [Ok] of the final value produced by the spawned thread, or [Err] of the value given to a call to panic if the thread panicked.

Note that there is no parent/child relationship between a thread that spawns a new thread and the thread being spawned. In particular, the spawned thread may or may not outlive the spawning thread, unless the spawning thread is the main thread.

Configuring threads

A new thread can be configured before it is spawned via the Builder type, which currently allows you to set the name and stack size for the thread:

use std::thread;

thread::Builder::new().name("thread1".to_string()).spawn(move || {
    println!("Hello, world!");
});

The Thread type

Threads are represented via the Thread type, which you can get in one of two ways:

  • By spawning a new thread, e.g., using the thread::spawnfunction, and calling thread on the JoinHandle.
  • By requesting the current thread, using the [thread::current] function.

The [thread::current] function is available even for threads not spawned by the APIs of this module.

Thread-local storage

This module also provides an implementation of thread-local storage for Rust programs. Thread-local storage is a method of storing data into a global variable that each thread in the program will have its own copy of. Threads do not share this data, so accesses do not need to be synchronized.

A thread-local key owns the value it contains and will destroy the value when the thread exits. It is created with the [thread_local!] macro and can contain any value that is 'static (no borrowed pointers). It provides an accessor function, [with], that yields a shared reference to the value to the specified closure. Thread-local keys allow only shared access to values, as there would be no way to guarantee uniqueness if mutable borrows were allowed. Most values will want to make use of some form of interior mutability through the [Cell] or [RefCell] types.

Naming threads

Threads are able to have associated names for identification purposes. By default, spawned threads are unnamed. To specify a name for a thread, build the thread with Builder and pass the desired thread name to Builder::name. To retrieve the thread name from within the thread, use Thread::name. A couple of examples where the name of a thread gets used:

  • If a panic occurs in a named thread, the thread name will be printed in the panic message.
  • The thread name is provided to the OS where applicable (e.g., pthread_setname_np inunix-like platforms).

Stack size

The default stack size is platform-dependent and subject to change. Currently, it is 2 MiB on all Tier-1 platforms.

There are two ways to manually specify the stack size for spawned threads:

  • Build the thread with Builder and pass the desired stack size to Builder::stack_size.
  • Set the RUST_MIN_STACK environment variable to an integer representing the desired stacksize (in bytes). Note that setting Builder::stack_size will override this. Be aware thatchanges to RUST_MIN_STACK may be ignored after program start.

Note that the stack size of the main thread is not determined by Rust.

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 function creates a thread with the default parameters of Builder. To specify the new thread’s stack size or the name, use Builder::spawn.

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::Errcontaining an opaque type.
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::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.

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::Result
Ok(T)

Contains the success value

codeintel::block_a8263751d50a25a9
fn main() -> Result<(), Box<dyn std::error::Error>>
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.

scope: &Scope<'_, '_>
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 function creates a thread with the default parameters of Builder. To specify the new thread’s stack size or the name, use Builder::spawn_scoped.

Panics

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