Write a function that return a task

Hi everybody. I'm trying to write a function that return a task, so that I use it like this:

tokio::task::spawn(myfunc(argrs))

If the arguments are borrowed is simple, but I need a reference, here is a sample

use futures::future::Future;

struct MyStruct {
    msg: String,
}

impl MyStruct {
    fn new(msg: String) -> Self { MyStruct{ msg  } }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
    let a = MyStruct::new("hello world".to_string());
    tokio::task::spawn(foo(&a)).await?;

    Ok(())
}

fn foo<'a>(ms: &'a MyStruct) -> impl Future<Output=()> + 'a {
    async move {
        println!("{}", &ms.msg);
    }
}

The problem is that this doesn't compile

   Compiling foo v0.1.0 (/Users/gecko/code/foo)
error[E0597]: `a` does not live long enough
  --> src/main.rs:14:28
   |
14 |     tokio::task::spawn(foo(&a)).await?;
   |                        ----^^-
   |                        |   |
   |                        |   borrowed value does not live long enough
   |                        argument requires that `a` is borrowed for `'static`
...
17 | }
   | - `a` dropped here while still borrowed

Since I'm awaiting the task, it seems at last to me that the reference lives long enough, don't?

Is there a way to tell the borrow checker that? Hey the future will be awaited before the referece is dropped,

Does async move {} blocks awaits expect 'static lifetime? it seems that I'm missing something here ....

task::spawn is conceptually same as thread::spawn. They require 'static lifetime because the newly spawned task/thread may live longer than the current task/thread.

Translating your code into threaded version would make understand easier:

std::thread::spawn(foo(&a)).join()?;

You spawn the task/thread and immediately await/join on it, does it make sense?

The sample doesn't make loots of sense, in practice I have something like this

    let t1 = tokio::task::spawn(foo(&a));
    let t2 = tokio::task::spawn(bar(&a));


   // a "main loop" doing all kinds of stuff
   // oops something happens, signal tasks to exit
   t1.await?
   t2.await?

Basically each task is a loop that read stuff and send to main thread through a channel, I have a configuration file, that becomes a Config struct that I want to share between tasks, this Config is what I'm trying to pass as a reference.

newly spawned task/thread may live longer than the current task/thread.

I see the main task is my misconception here, do I need something like an Arc so?

You can't do that since tokio::task::spawn only takes 'static tasks, as the compiler tells you.

The preferred solution is to use Future::join instead of spawning (and join both those futures plus the "main loop" future); alternatively you need something like the async-scoped crate (or you could unsafely transmute to 'static, but that's obviously unsafe and inelegant).

2 Likes

Sorry, you can't put references into tokio::spawn. Perhaps you want the join! macro instead, as it allows running things concurrently while allowing references. You can also share the object using an Arc instead of a reference.

2 Likes

Thanks @netns and @alice for the answers.

In this case I want to tasks to run apart from one each other, if some task hangs I want others to keep going on ... so I think that the way to go is using an Arc, thanks again!

For a matter of completeness here is how the code looks with Arc

use futures::future::Future;
use std::sync::Arc;

struct MyStruct {
    msg: String,
}

impl MyStruct {
    fn new(msg: String) -> Self { MyStruct{ msg  } }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
    let a = MyStruct::new("hello world".to_string());
    let a = Arc::new(a);
    tokio::task::spawn(foo(a)).await?;

    Ok(())
}

fn foo<'a>(ms: Arc<MyStruct>) -> impl Future<Output=()> {
    async move {
        println!("{}", ms.msg);
    }
}