multithreadingrustclosureslifetime

Why does Rust consider an owning closure to exist for 'static lifetime?


I was considering the possibility of passing a closure that owns some thread-safe value, to a spawned thread. The thread would then be able to call something only knowing the signature, while the contents of it is opaque to it.

I wrote a simple test, and surprisingly, it works. For the closure to be callable inside the thread, the Rust compiler suggested adding std::marker::Send and 'static constraints to it. The first one being applicable makes sense -- all values that the closure captures are Send, but the static lifetime requirement being consdered satisfied confused me, since I assumed the compilier would consider the closure and the value it owns to have a specific non-static lifetime.

I would like to understand why. Here is the code:

use std::sync::{mpsc::{Receiver, Sender}, Arc, Mutex};

fn main() {
    let tx = start_thread();

    // can also run in a loop with a sleep in between or whatever
    let _ = tx.send(String::from("Something"));
}

fn start_thread() -> Sender<String> {
    // Something created on the main thread that we pass to the spawned thread
    //  inside the closure -- the spawned thread has no direct knowledge of it 
    let closure_owned_value: Arc<Mutex<f32>> = Arc::new(Mutex::new(42.0));
    
    // A closure we want to be called by the spawned thread, 
    // but not defined there -- this is considered 'static somehow?
    let on_thread_callback = move |msg: String| {
        println!("Got back message from thread: {:?}, owned value: {:?}", msg, closure_owned_value);
    };

    let (tx, rx) = std::sync::mpsc::channel();

    spawn_thread_with_callback(rx, on_thread_callback);

    tx
}

fn spawn_thread_with_callback<F>(
    rx: Receiver<String>,
    callback: F
) -> std::thread::JoinHandle<()>
where
    F: Fn(String) -> () + std::marker::Send + 'static
{
    std::thread::spawn(move || {
        /* Run an infinite loop as long as channel is open. */
        while let Ok(message) = rx.recv() {
            println!("Thread received message: {:?}", message);
            callback(String::from("Hello back from thread!"));
        }
    })
}

Solution

  • The 'static lifetime you are referring to might be sort of misleading.

    In your function declaration, you have F: Fn(String) -> () + std::marker::Send + 'static. For simplicity, we can just consider what does F: 'static mean.

    Note that you don't pass the callback via a 'static reference (you have callback: F rather than callback: &'static F). The meaning of these two is completely different: rather than expressing "this reference must live until the end of the program" (which &'static F does), F: 'static means "this value must be able to live until the end of the program (but doesn't necessarily have to actually live until then)". In practice, this means that inside F there must not be any non-'static references. This means that i32 is 'static, String is 'static, but struct Foo<'a> { bar: &'a str } (where a is not static) is not.

    Now, consider what happens in your code. You create a closure that captures Arc<Mutex<f32>> by moving. It is therefore not deconstructed at the end of start_thread's scope; instead, it's deconstructed whenever the closure is deconstructed. Note also that you cannot use closure_owned_value after creating on_thread_callback — since you passed the Arc by moving, it's no longer available in the scope and trying to use it will result in a compilation error (exactly because its ownership has been moved). Arc can easily live until the end of the program — it is a reference-counted reference under the hood, which behaves like you have a 'static reference to the value underneath; the only difference is that it's automatically dropped when no longer reachable in code.