Problem with worker thread which has same life time as main thread

Ok, Consider my spaceship has just landed into Rust Plant after long trip from C++ planet!

In C++ is common to have an App instance which holds most of the modules. Probably it has three main methods: one for initializing modules, one for starting them and one for closing or stopping them.

Starting modules can run modules in worker threads that has almost same lifetime as main thread.
It's kind of bad design pattern in Rust. I know. I would like to know what is your suggestion.

Code in Rust Play Ground

I have a piece of code in my spaceship which is translated from C++ into rust. But it doesn't work. My space ship is not working now unless I found a solution for that.

I appreciate your help.

You will first have to move NoSend into the spawned future rather than borrow it. Then you will have to change NoSend::run to take self instead of &self. For example Rust Playground

Even ignoring the issues with Send, this can never work.

pub fn start(&self) -> task::JoinHandle<()> {
    task::spawn(async {
        self.engine.run().await;
    })
}

All spawned tasks must have full ownership of all values stored in the task, but you do not have ownership of self to give the new task.

Generally you are going to need to structs: A struct owned by the task, and a handle struct that can communicate with that task. Consider the following example:

use tokio::task;
use tokio::sync::mpsc::{self, Receiver, Sender};

struct Engine {
    counter: i32,
    recv: Receiver<EngineMsg>,
}

impl Engine {
    pub async fn run(&mut self) {
        while let Some(msg) = self.recv.recv().await {
            match msg {
                EngineMsg::IncrAndPrint => {
                    self.counter += 1;
                    println!("{}", self.counter);
                }
            }
        }
        // when the sender is dropped, the while loop exits
    }
}

enum EngineMsg {
    IncrAndPrint,
}
#[derive(Clone)]
struct EngineHandle {
    msg: Sender<EngineMsg>,
}
impl EngineHandle {
    pub fn new() -> Self {
        let (send, recv) = mpsc::channel(16);
        
        let mut engine = Engine {
            counter: 0,
            recv,
        };
        tokio::spawn(async move {
            engine.run();
        });
        
        EngineHandle {
            msg: send,
        }
    }
    pub fn incr_and_print(&mut self) {
        self.msg.send(EngineMsg::IncrAndPrint);
    }
}

This also removes the need for RefCell so any issues with Send are gone.

Shut down is handled by simply running the destructor of all senders to the channel.

2 Likes

Thanks. You always come with out-of-box solutions. Now I have better understanding about it.

1 Like

main() in Rust isn't special, and the borrow checker will assume other threads can outlive it.

If you need a singleton-like thing that lives for the entire duration of the program, an easy hack is to use Box::leak(). Yes, it literally leaks memory, but in return you get an immortal reference that the borrow checker will allow almost anywhere.

Otherwise you'll need to wrap your App in Arc<App>, so that you can Arc::clone() it whenever you pass it to any task/thread that could theoretically outlive main for a brief moment.

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.