Running a background future that access self

Hi there! I'm building a timer application using Tauri, and I'm trying to run a future in background, but I'm having troubles with being able to (1) stop the future when I stop the timer, (2) use self inside the interval.

I'll paste my current code below, but I also want to understand if there's a better way of doing what I'm trying to do. My initial attempt was working, but I had a timer future running all the time and I'd pass it to my AppState, which was fine, but I wasn't able to synchronize the start with the UI (since the timer was always running, I could start counting the time in between seconds).

This is some sort of diagram of my app:

And here's my latest attempt at getting the timer running from inside the app state

use core::time::Duration;
use std::sync::Arc;
use tokio::task::JoinHandle;
use core::sync::atomic::AtomicI32;

pub struct AppState {
    pub time: AtomicI32,
    timer_handle: Option<JoinHandle<()>>,
}

impl AppState {
    pub fn new() -> Self {
        Self {
            time: AtomicI32::new(0),
            timer_handle: None,
        }
    }

    pub async fn start(mut self) {
        let handle = tokio::spawn(async move {
            let mut interval = tokio::time::interval(Duration::from_secs(1));

            loop {
                interval.tick().await;

                self.tick().await;
            }
        });

        self.timer_handle = Some(handle);
    }
    
    pub async fn tick(self) {
        println!("Ticking...")
    }
}

fn main() {
    println!("Hello, world!");
    
    let app_state = AppState::new();
    
    app_state.start();
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
warning: unused import: `std::sync::Arc`
 --> src/main.rs:2:5
  |
2 | use std::sync::Arc;
  |     ^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

error[E0382]: use of moved value: `self`
  --> src/main.rs:26:17
   |
26 |                 self.tick().await;
   |                 ^^^^ ------ `self` moved due to this method call, in previous iteration of loop
   |
note: this function takes ownership of the receiver `self`, which moves `self`
  --> src/main.rs:33:23
   |
33 |     pub async fn tick(self) {
   |                       ^^^^
   = note: move occurs because `self` has type `AppState`, which does not implement the `Copy` trait

error[E0382]: assign to part of moved value: `self`
  --> src/main.rs:30:9
   |
19 |       pub async fn start(mut self) {
   |                          -------- move occurs because `self` has type `AppState`, which does not implement the `Copy` trait
20 |           let handle = tokio::spawn(async move {
   |  ______________________________________________-
21 | |             let mut interval = tokio::time::interval(Duration::from_secs(1));
22 | |
23 | |             loop {
...  |
26 | |                 self.tick().await;
   | |                 ---- variable moved due to use in generator
27 | |             }
28 | |         });
   | |_________- value moved here
29 |
30 |           self.timer_handle = Some(handle);
   |           ^^^^^^^^^^^^^^^^^ value partially assigned here after move

For more information about this error, try `rustc --explain E0382`.
warning: `playground` (bin "playground") generated 1 warning
error: could not compile `playground` due to 2 previous errors; 1 warning emitted

First of all, you should get used to the ownership system. Having a method take self rather than &self/&mut self (or other more complex variations) means that it takes ownership of your instance of AppState, or in other words, you can no longer use that instance after having called the method (it will only be available inside that method). This is where most of your errors currently come from. I suggest you to get comfortable with ownership, borrowing and lifetimes before starting more complex projects, especially ones that use async and multithreading, since those are the ones with the worst error messages and you could easily get overwhelmed.

After you pass this stage and try changing those self to &self/&mut self, you'll see that you'll have difficulties trying to make lifetimes match, and that's because having a struct being accessible from a background task is not something trivial lifetime wise, you need to ensure statically that the struct will still be valid when that something in background comes back and touches the struct. Usually this is solved by putting your struct in an Arc.

1 Like

I get that, and yes, I'm still trying to understand how everything fits together, but I still want to work on this project :blush:

So, this is what I had tried already, but I was getting an issue with assigning the handle to the arc value. I just realised that I could use a mutex for the timer handle, like this:

    pub async fn start(self: Arc<Self>) {
        let timer_state = self.clone();

        let handle = spawn(async move {
            let mut interval = tokio::time::interval(Duration::from_secs(1));

            loop {
                interval.tick().await;

                println!("Ticking");

                timer_state.tick().await;
            }
        });

        *self.timer_handle.lock().await = Some(handle);

        println!("Started...");

        self.emit_state_update().await;
    }

now I should be able to stop the timer inside my stop method :blush:

Does this code make sense? That was my primary concern to be honest :blush:

Yeah, that should work.

Depending on the access patterns on self.timer_handle you might also use a normal sync Mutex. tokio's Mutex documentation page has a section where it describes when you should use one over the other.

Perfect, thank you!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.