Using Arc<()> to terminate a thread

Thank you for all the responses so far.

I tried to make things more "idiomatic", and came up with this:

use std::thread::{sleep, spawn};
use std::time::Duration;

pub mod keepalive {
    use std::sync::{Arc, Weak};

    pub struct Sender(Arc<()>);

    #[derive(Clone)]
    pub struct Receiver(Weak<()>);
    
    pub fn channel() -> (Sender, Receiver) {
        let arc = Arc::new(());
        let weak = Arc::downgrade(&arc);
        (Sender(arc), Receiver(weak))
    }
    
    impl Receiver {
        pub fn is_alive(&self) -> bool {
            Weak::strong_count(&self.0) > 0
        }
    }
}

fn main() {
    let (keepalive_send, keepalive_recv) = keepalive::channel();
    let join_handle = spawn(move || {
        let mut counter = 0;
        while keepalive_recv.is_alive() {
            // do something here:
            counter += 1;
            println!("running ({counter})...");
            sleep(Duration::from_millis(150));
        }
        counter
    });
    // do something here:
    sleep(Duration::from_millis(1000));
    // terminate spawned thread:
    drop(keepalive_send);
    let result = join_handle.join().unwrap();
    println!("Result: {result}");
}

(Playground)

Here, the child thread cannot "accidentally" keep itself alive, yet it's possible to clone the receiver if desired. But it's not possible to clone the sender.

I'm not sure if that makes things really better (unless it was hidden in some crate, or perhaps in future be part of std or tokio::sync in this or a similar form). What I liked about Arc<()> is that it's so simple, but I also feel like it's a bit confusing / abusive.

I guess what would better is a Relaxed ordering, which would be even more efficient, right? There is Arc::try_unwrap, which does a Relaxed(!) compare_exchange in the error case (and will only acquire if being successful). Maybe that is faster?

Let's try it without hiding the details, just simply using Arc:

use std::sync::Arc;
use std::thread::{sleep, spawn};
use std::time::Duration;

fn main() {
    let keepalive_send = Arc::new(());
    let mut keepalive_recv = keepalive_send.clone();
    let join_handle = spawn(move || {
        let mut counter = 0;
        loop {
            match Arc::try_unwrap(keepalive_recv) {
                Ok(()) => break,
                Err(err) => keepalive_recv = err,
            }
            // do something here:
            counter += 1;
            println!("running ({counter})...");
            sleep(Duration::from_millis(150));
        }
        counter
    });
    // do something here:
    sleep(Duration::from_millis(1000));
    // terminate spawned thread:
    drop(keepalive_send);
    let result = join_handle.join().unwrap();
    println!("Result: {result}");
}

(Playground)

@Michael-F-Bryan: I don't see how I can "interrupt" the expensive_background_computation in your example.

I feel like none of the solutions presented so far get to the simplicity of the original idea. However, I understand that wrapping everything in a newtype pattern could make semantics more clear.