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!"));
}
})
}
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.