C.W.K.
Stream
Lesson 02 of 05 · published

move Closures Across Threads

~10 min · concurrency, move, ownership

Level 0Rust Curious
0 XP0/80 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete

A spawned thread can outlive the scope that created it. So when its closure uses data from that scope, Rust faces a question: will the data still be alive when the thread reads it? The move keyword answers it.

Why threads need move closures

If a thread closure borrowed a local variable, and the spawning function returned before the thread finished, the borrow would dangle — the classic use-after-free. Rust refuses to compile that. The fix is move: the closure takes ownership of what it captures, so the data lives as long as the thread does, independent of the original scope.

Ownership crosses the thread boundary

With move, captured values are transferred into the thread. The spawning thread can no longer use them — they belong to the new thread now. This is the same move semantics from the Ownership track, applied across a thread boundary, and it's exactly what makes the transfer safe: only one thread owns the data.

move makes the thread own its data, so nothing can dangle. By transferring ownership into the closure, Rust guarantees the data outlives the thread and isn't touched by the spawner anymore. It's the borrow checker's dangling-reference rule, enforced across threads.

What about sharing, not moving?

Sometimes you don't want to give data to one thread — you want several threads to share it. You can't move one value into many closures. That's where Arc (from the Smart Pointers track) comes back: clone an Arc per thread, and each thread owns a shared handle to the same data. The next two lessons build on exactly that.

Code

move transfers ownership into the thread·rust
use std::thread;

fn main() {
    let data = vec![1, 2, 3];

    // `move` transfers ownership of `data` into the thread
    let handle = thread::spawn(move || {
        println!("thread owns {data:?}");
        data.iter().sum::<i32>()
    });
    // data can't be used here anymore — the thread owns it now

    let total = handle.join().unwrap();
    println!("sum: {total}");
}

External links

Exercise

Create a String, then spawn a thread with a move closure that takes ownership of it and prints it. Confirm you can't use the String in main afterward. Then remove move and read the borrow error — what exactly is the compiler worried about?
Hint
Without move, the closure borrows the String, but the thread might outlive main's String scope — a dangling borrow. move transfers ownership so the thread keeps the data alive as long as it needs.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.