FnOnce Return Value Lifetime Problems (parameter T may not live long enough)

I'm currently experimenting with threads and channels. I've taken the web server thread pool example from the Rust book(2018) and modified it, and now I'm having problems and I'm not sure how to resolve this.

The book, on page 484, creates an FnBox trait which will allow the application to Box an FnOnce instance and defines a call_box function which gives the trait the ability to call itself. I modified this so that the closure returns a value:

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

//  Added 'type Return'
pub trait FnBox {
    type Return;
    fn call_box(self: Box<Self>) -> Self::Return;
}

impl<T, F: FnOnce() -> T> FnBox for F {
    type Return = T;
    fn call_box(self: Box<F>) -> Self::Return {
        (*self)()
    }
}

type Job<T> = Box<dyn FnBox<Return = T> + Send + 'static>;

So far this is good, I get this. My problem is using this trying to call this closure defined in Job in spawned threads. I plan to get the return value of the closure and send it through an other channel, but I haven't gotten there yet.


pub struct ThreadPool<T: Send> {
    pool: Vec<Worker>,
    pub sender: Sender<Job<T>>,
}

impl<T: Send> ThreadPool<T> {
    pub fn new(result_sender: &Sender<T>) -> Self {
        let (sender, receiver) = mpsc::channel();
        let mutex = Arc::new(Mutex::new(receiver));
        let mut pool = vec![];
        for i in 0..4 {
            pool.push(Worker::new(i, &mutex));
        }

        Self { pool, sender }
    }
}

struct Worker {
    thread: JoinHandle<()>,
}

impl Worker {
    pub fn new<T>(id: usize, receiver: &Arc<Mutex<mpsc::Receiver<Job<T>>>>) -> Self
    {
        let recv = receiver.clone();

        let thread = thread::spawn(move || loop {
                           //^^^ Error Here: the parameter type 'T' may not live long enough
            match recv.lock().as_ref() {
                Ok(&mutex_recv) => {
                    if let Ok(r) = mutex_recv.try_recv() {
                        let _ = r.call_box();
                           // ^^^ I plan to use this return value later
                    }
                }
                Err(e) => {
                    eprint!(
                        "Error occurred trying to get Arc lock in thread {}: {}",
                        id, e
                    );
                    return;
                }
            }
        });
        Self { thread }
    }
}

So, from what I gather from this error, is that the compiler doesn't know whether the closure will be returning a reference or not. And if the closure is returning a reference then it really needs to know the lifetime involved.

What I want is to tell the compiler is that the closure is giving ownership of the return value to the caller and won't be returning a reference. If I can figure this out I think this will resolve this compile error.

Edit: Here is a link to the playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=093270ff689f6fe810762a2f04ee2fe7

The standard thread::spawn can only deal with 'static types, but you're trying to make an arbitrary lifetime in T: 'a. It's not exactly worried that T may be a reference, but that T can be any type that has borrowed something for the lifetime 'a, while the thread may stay alive for the rest of the program ('static) holding that borrow in T too long.

Your example does work if it is constrained to T: 'static.

2 Likes

Protip: You don't need a trait FnBox as we have impl FnOnce for Box<dyn FnOnce> since the Rust 1.35, released at may 23, 2019.

4 Likes

Thank you!

Hmm, this worked, but now I'm a little confused. I thought this lifetime of the value returned from the closure(FnOnce). Is it not? From the way I'm reading this, giving T a lifetime of static is basically saying "The lifetime of the value returned from the closure is static. Is this assumption wrong?

T: 'static means that values of type T can live a 'static lifetime, that T doesn't borrow anything that would invalidate it sooner. The owner of each T value gets to decide what to do with it.

For example, T = Vec<i32> is a 'static type. You can hold onto that vector however long you want, unconstrained by any other lifetime. But T = Vec<&'a i32> is constrained by that lifetime, so the vector can only be used as long as that borrowed 'a lasts, which the compiler checks.

When you have a T: 'a constraint, this is like a lower bound on those who provide a T, that this value must be valid for the entire lifetime 'a. It's an upper bound on those using a T, since it could be invalid soon after, like a dangling reference.

4 Likes

mind_blown.gif

It all makes sense now. Thanks!