Treat thread return type as soon as it ends without waiting for it

#[derive(Debug, Default)]
pub struct Quit {
    pub bool: bool,
}

impl Quit {
  pub fn handle_events(self) -> Arc<Mutex<Quit>> {
          let quit: Arc<Mutex<Quit>> = Arc::new(Mutex::new(self));
          {
              let quit = Arc::clone(&quit);
              thread::spawn(move || {
                  loop {
                      match event::read() {
                          Ok(Event::Key(key_event)) if key_event.kind == KeyEventKind::Press => {
                              match key_event.code {
                                  KeyCode::Char('q') => {
                                      Quit::quit(&quit);
                                  }
                                  _ => {}
                              }
                          }
                          Err(_) => {} //HANDLING NEEDED TODO
                          _ => {}
                      };
                  }
              });
          }
  
          return quit;
  }
}

fn main() -> anyhow::Result<()> {
    let quit = Quit::default().handle_events(); //ERROR HANDLING TODO

    // do stuff with quit WHILE THE THREAD IS RUNNING and IF handle_events() 
    //inner thread returns an error, UNWRAP IT AND FINISH THE PROGRAM
}

First of all, is it possible?

I know we have to use .join() to deal with the return type of the thread, but this makes the main thread wait for the other thread to end and THEN do something to the returned value. I want the caller thread to keep doing other stuff until it is returned.

I've thought of two solutions:

  1. turning Quit into an enum, so if it matches Err(_), use the other enum type that means that an error happened
  2. using a channel (Bad because only one Err(_) should be enough to stop the program, cost is too high)

But I feel like there are better options and I'm taking any good advice even if it means modifying the function format. Thank you!

Why would the cost be too high for a channel? You’re using a Mutex<bool> instead of the more efficient AtomicBool.

In any case, the best you can do with this design (without joining the thread) is polling the state for changes (aka pull). In the case of a channel, that’s try_recv(), with Mutex<bool> it’s periodically locking the mutex, and with AtomicBool it’s an atomic load(). None of these are particularly expensive if you are doing the polling infrequently (every 10ms or 100ms, for instance).

A channel has the added benefit that you can select!() over multiple sources. Which changes the polling from frequency-based into a dynamic pull across whichever source happens to be ready first.

async fn is sugar over callbacks (aka push). The major advantage is that any future can be used with select!(), not just channel sources. Raw callbacks are kind of miserable to use effectively (you still have to poll for state changes in your main loop).

Another polling option is to keep the JoinHandle around and have the thread return errors (poll with join_handle.is_finished() and join).

Quick example. I don't know the relative costs.

I thought a channel would be a bad choice as it's usually used for sharing multiple values, but in this code only one is needed.

Hey this looks like a really good option. I wonder if its cost is higher than channels or not. Will try to find out

I don’t really see why the extra cost matters? It’s not like using a channel is going to eat into your workload time. All you’re using it for is a simple notification for the workload to stop what it’s doing. That’s hardly going to matter compared to the rest of the code, honestly.

Honestly I just think I'm just overly worried about something that doesn't matter that much.. I'll be going with your idea, thank you

edit: Even because it will only run when the program is already about to return an error, so, basically 0 importance