rustasync-awaitrust-async-std

Creating an asynchronous task from a method, from within a method of the same structure


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


Solution

  • 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.