0

How can one method of a structure run another method of the same structure as a task?

Forgive me for possible stupidity, I'm new to Rust. The compiler complains that self cannot survive the body of the function in which it is called. But isn't this accessing the context of a structure that is, by definition, alive, since its methods can be called?


use std::time::{Duration};
use async_std::{task, task::JoinHandle, prelude::*};


async fn sleep(delay: f64) -> () {
    task::sleep(Duration::from_secs_f64(delay)).await;
}

struct TestTask {
    item: usize,
    count: usize
}

impl TestTask {
    fn new() -> TestTask {
        return Self { item: 0, count: 0 }
    }

    async fn task(&mut self, delay: f64) -> () {
        sleep(delay).await;
        println!("AFTER SLEEP {}sec, value = {}", delay, self.item);
        self.item += 1;
    }

    fn create_task(&mut self, delay: f64) -> () {
        task::spawn(self.task(delay));
        self.count += 1;
    }
}

async fn test_task() -> () {
    let mut obj = TestTask::new();
    let delay: f64 = 5.0;
    obj.create_task(delay);

    sleep(delay + 1.0).await;
    println!("DELAY SLEEP {:?}sec", delay + 1.0);
}

#[async_std::main]
async fn main() {
    test_task().await;
}

enter image description here

KusochekDobra
  • 23
  • 1
  • 3
  • `task::spawn` [here](https://docs.rs/async-std/latest/async_std/task/fn.spawn.html) requires that the future passed to it be bounded by `'static`. This isn't the case since `&mut self` has a non-static lifetime. It's because `async_std` typically uses a multi-threaded an executor, and even though you can configure it to use a single thread, the `task::spawn` API still enforces the `'static` bound. – jsstuball Sep 19 '22 at 09:32
  • [Please do not upload images of code/data/errors when asking a question.](//meta.stackoverflow.com/q/285551) – Jmb Sep 19 '22 at 09:41
  • For additional clarity, the async executor can move tasks between threads in it's async thread pool. So fwiw if you had configured a single-threaded executor then the `'static` bound is unnecessarily restrictive. But otherwise it's there because in Rust, without scoped threads, the compiler can't reason about the lifetimes of things you send into other threads. – jsstuball Sep 19 '22 at 09:45
  • I feel like this question comes up every couple of days (spawning a thread/task with a reference), yet I can't seem to find a proper duplicate... – Finomnis Sep 19 '22 at 10:11
  • @Finomnis possibly [this](https://stackoverflow.com/questions/72182515/rust-argument-requires-that-variable-is-borrowed-for-static) (which you answered), or one of the duplicates linked there. – Jmb Sep 19 '22 at 10:29
  • @Jmb Maybe I should create a dummy question and answer that shows this in its pure form – Finomnis Sep 19 '22 at 11:18

1 Answers1

1

The Problem:

The compiler does not know if the task created in create_task ever ends. This is a problem because after spawn is called the task runs asynchronously from the rest of the program. So it is possible that self.count += 1 will run first (this is most likely to happen), but on the other hand self.item += 1 can also run first. The problem here is that self.item += 1 could be executed so late that the original object has already been deallocated and the reference to it is only a dangling pointer.

Reference Counters:

One possibility would be to wrap the object in a reference counter, so that its lifetime would no longer be determined statically by the compiler but would be decided dynamically during runtime.

use std::time::Duration;
use tokio;
use tokio::time;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::sync::atomic::AtomicUsize;

#[derive(Default, Debug)]
struct TestTask {
    item: AtomicUsize,
    count: AtomicUsize
}


impl TestTask {
    async fn task(self: Arc<TestTask>, delay: f64) {
        let duration = Duration::from_secs_f64(delay);
        time::sleep(duration).await;
        println!(
            "AFTER SLEEP {}sec, value = {}",
            delay,
            self.item.fetch_add(1, Ordering::Relaxed)
        )
    }
    
    fn create_task(self: Arc<TestTask>, delay: f64) {
        tokio::spawn(self.clone().task(delay));
        self.count.fetch_add(1, Ordering::Relaxed);
    }
}

#[tokio::main]
async fn main() {
    let obj = Arc::new(TestTask::default());
    let delay: f64 = 5.0;
    obj.create_task(delay);
    
    time::sleep(Duration::from_secs_f64(delay + 1.0)).await;
    println!("DELAY SLEEP {:?}sec", delay + 1.0);
}

I used tokio here because async_std is not available in the playground. If usize is required instead of it‘s atomic counterpart the object needs to be wrapped in a mutex.

You may also want look into scoped threads, which effectively allow you to pass non static references into different threads or tasks.

Goldenprime
  • 324
  • 1
  • 10