How to tell lifetime checker that tasks in Joinset will be aborted before disconnected?

I'd like to spawn tasks with tokio::task::JoinSet that references enclosing struct as follows.

    async fn connect(&self) {
        let mut state = self.state.lock().await;
        let mut tasks = JoinSet::new();
        tasks.spawn(async move {
            loop {
                let state = self.state.lock().await;
                match *state {
                    State::Done => {
                        println!("done");
                        break;
                    }
                    _ => {
                        println!("I/O simulation");
                        sleep(Duration::from_millis(1000));
                    }
                }
            }
        });
        *state = State::Running(tasks);
    }

For full code & test: Rust Playground

However, I'm seeing following compilation error from lifetime checker.

error[E0521]: borrowed data escapes outside of method
  --> src/main.rs:19:9
   |
16 |       async fn connect(&self) {
   |                        -----
   |                        |
   |                        `self` is a reference that is only valid in the method body
   |                        let's call the lifetime of this reference `'1`
...
19 | /         tasks.spawn(async move {
20 | |             loop {
21 | |                 let state = self.state.lock().await;
22 | |                 match *state {
...  |
32 | |             }
33 | |         });
   | |          ^
   | |          |
   | |__________`self` escapes the method body here
   |            argument requires that `'1` must outlive `'static`

I can understand that Rust may not know the task will be aborted and dropped when encosing JoinSet is dropped, but I couldn't find a way to provide such information.

So my questions are:

  • Can I fix the compilation error by specifying lifetime? I tried some already (e.g. JoinSet::<'a>::new()) but it didn't work neither.
  • If it's unavoidable, is there an alternative? Should I wrap the tokio::sync::Mutex with Arc? I thought that Arc is unnecessary because tokio's getting started guide (link) doesn't mention it. (maybe a newbie mistake?)

I think this is the easiest way to make your code compile and share the state between your task and your struct.

An alternative approach you could try would be message passing instead of sharing state, i.e. with a oneshot channel.

1 Like

Nope. Because JoinSet::spawn requires the future impls 'static. This is the signature.

pub fn spawn<F>(&mut self, task: F) -> AbortHandle
where
    F: Future<Output = T> + Send + 'static,
    T: Send
;

Yes. It's the easiest and maybe the most correct way to let your code compiles.

If you don't want to use synchronization tools, let's get back to the origin: what do you want to do with JoinSet? If you just want to execute serveral tasks in the same time, I would suggest futures::future::join_all.

1 Like

If you don't want to use synchronization tools, let's get back to the origin: what do you want to do with JoinSet? If you just want to execute serveral tasks in the same time, I would suggest futures::future::join_all.

Thank you for the answer. I only want to know whether tokio has its own version of Arc or Arc alternative as it does for other APIs (e.g. Mutex, File, etc)

It hasn't. Because Arc will never block the thread, for it was two AtomicUsizes inside. That is to say, no .await is needed for Arc and no special Arc in tokio.

Synchronization tools like Mutex has async version is because std Mutex may block the thread. If you're sure in your scenario std Mutex never block the thread, use std Mutex instead of tokio Mutex.

1 Like