Threads

move Closures and Threads

The move keyword before a closure's parameter list (move || { ... }) forces the closure to take ownership by value (or bitwise copy for Copy types) of every variable or struct field it references from its enclosing environment, rather than capturing them by reference (&T or &mut T).

Selective Capture with Shadowing Before move ||

Because move || moves every variable mentioned inside the closure body, referencing config.port or Arc::clone(&shared) inside the move || body would move the entire config or original shared variable into the closure!

To capture only a specific cloned handle, a Copy field, or a reference (inside thread::scope) while keeping the rest of the variables in the outer scope, bind the exact values you want the closure to capture right before move ||:

Pitfall — Moving the Original `Arc` Inside `move ||`:
  let data = Arc::new(vec![1, 2]);
  thread::spawn(move || {
      let c = Arc::clone(&data); // <-- Moves `data` itself into the closure!
  });
  // `data` is no longer usable in the parent thread!

Idiomatic Pattern — Clone Outside, Move the Clone Inside:
  let data = Arc::new(vec![1, 2]);
  let data_for_worker = Arc::clone(&data);
  thread::spawn(move || {
      println!("{:?}", data_for_worker); // <-- Only `data_for_worker` is moved!
  });
  // `data` remains owned by the parent thread!
use std::sync::Arc;
use std::thread;

struct ServerConfig {
    name: String,
    port: u16,
}

fn main() {
    let config = ServerConfig {
        name: String::from("gateway-api"),
        port: 8080,
    };
    let shared_routes = Arc::new(vec!["/health", "/metrics"]);

    // 1. Copy only the `u16` field (`port` is `Copy`, so `config.name` stays in `main`!):
    let port = config.port;
    // 2. Clone the `Arc` outside the `move ||` closure:
    let routes = Arc::clone(&shared_routes);

    let handle = thread::spawn(move || {
        println!("worker serving {:?} on port {}", routes, port);
    });

    // Both `config.name` and `shared_routes` remain available on the main thread!
    println!(
        "main still owns config.name = {:?} and {} routes",
        config.name,
        shared_routes.len()
    );

    handle.join().unwrap();
}
main still owns config.name = "gateway-api" and 2 routes
worker serving ["/health", "/metrics"] on port 8080

Disjoint Field Capture in move Closures (Rust 2021 and 2024 Editions)

Starting in Rust 2021 (and continued in Rust 2024), move closures perform disjoint field capture: if a move closure only mentions pair.0, it moves only pair.0 into the closure and leaves pair.1 owned by the outer function!

use std::thread;

struct Pair {
    left: String,
    right: String,
}

fn main() {
    let pair = Pair {
        left: String::from("moved to thread 1"),
        right: String::from("moved to thread 2"),
    };

    // Disjoint field capture moves `pair.left` into `t1` and `pair.right` into `t2`:
    let t1 = thread::spawn(move || println!("t1: {}", pair.left));
    let t2 = thread::spawn(move || println!("t2: {}", pair.right));

    t1.join().unwrap();
    t2.join().unwrap();
}
t1: moved to thread 1
t2: moved to thread 2

Combining move with References Inside thread::scope

Inside std::thread::scope, you often want a closure to move a loop index i: usize by value while borrowing a large collection &data by reference. Because a reference &T is itself a Copy value, binding let data_ref = &data; outside s.spawn(move || ...) moves i by value and copies the reference data_ref into the closure!