Docs The language Ownership & borrows spawn()
v0.14.2
💬

spawn() — sharing references
across threads

A walk-through of how spawn() moves ownership of a borrowed reference into a new thread, while preserving the compile-time guarantees of the borrow checker.

📖 Reading time: 14 min📦 Module: core::thread🪧 Since: v0.12.0🛠 Last edited: 4 days ago

What it does

The spawn primitive creates a new thread and runs the given closure inside it. Unlike its equivalents in C++ or Rust, the borrow checker enforces that the closure does not outlive the longest borrow of any reference it captures — at compile time, with no runtime overhead.

⚡ Quick note

If you have used Rust's std::thread::spawn, the model here is similar but uses scoped threads by default. There is no need to wrap calls in thread::scope.

The signature

spawn
(f: impl FnOnce() -> T + Send + 'a) -> JoinHandle<'a, T>

Parameters

f: impl FnOnce() -> T + Send + 'arequired
The closure to run on the new thread. Must be Send, since it crosses thread boundaries, and must outlive lifetime 'a, where 'a is bounded by the scope in which spawn is called.
T: type parameter
The closure's return type, inferred from the body of f. Must also be Send.

A worked example

The most common use is sharing a slice of an in-scope array across multiple workers. This compiles, because the borrow checker can see that every spawn returns before main exits:

// main.orbuse core::thread::spawn;

fn main() {
  let data: [i32; 4] = [1, 2, 3, 4];
  let half = data.len() / 2;

  // Each spawn borrows &data, not a copy.
  let h1 = spawn(|| sum(&data[..half]));
  let h2 = spawn(|| sum(&data[half..]));

  // Joins block until both threads finish.
  let total = h1.join() + h2.join();
  println("total = {}", total);
}

fn sum(xs: &[i32]) -> i32 {
  xs.iter().sum()
}

Why this compiles

Both closures borrow data immutably. The compiler synthesizes a lifetime 'a that bounds the longest-lived JoinHandle by the enclosing scope. Since both handles are joined before main() returns, 'a is at least as long as the data, and the borrow checker accepts the program.

If you try to return either handle from a function that owns data, you will get the compile-time error E142: handle outlives borrow — and a suggestion to either move ownership into the thread or use core::sync::Arc.

⚠ Common pitfall

Don't try to capture &mut data from two threads at once. The borrow checker will refuse, even if the slices are non-overlapping. Use data.split_at_mut(half) first, then capture each half separately.

Performance

Thread spawn is implemented as a direct OS-thread (pthread on Unix, CreateThread on Windows). For high-throughput workloads, prefer core::async::task::spawn which uses a multi-threaded runtime under the hood.