sholsapp
sholsapp

Reputation: 16070

Design help: threading within a struct

I'm new to Rust. As a learning exercise I'm trying to write a simple timer struct that I once wrote in C++. The interface and implementation looks something like this:

pub struct Timer {
    handle: Option<std::thread::JoinHandle<()>>,
    alive: bool,
}

impl Timer {
    pub fn new() {
        Timer {
            handle: None,
            alive: false,
        }
    }

    pub fn start(&'static mut self) {
        // Oops! How do I do this?
        self.handle = Some(std::thread::spawn(move || {
            self.alive = true;
            self.loop()
        }));
    }

    pub fn stop(&mut self) {
        self.alive = false;
        self.handle.unwrap().join()
    }

    pub fn loop(&self) {
        // while alive
    }
}

I understand why this is an error because of use of moved value: self within the start function, but I'm wondering how I'm supposed to design my struct so that something like this would work. In every scenario I can think of, I'll always have a double borrow situation.

I have a hunch that I need to learn more about interior mutability, but figured I would ask for design guidance before going down any more rabbit holes.

Upvotes: 12

Views: 7302

Answers (1)

Matthieu M.
Matthieu M.

Reputation: 299730

I think you are pretty close to getting it to work.

There are only two hurdles:

  • thread::spawn will not allow sharing references
  • alive and loop for you to share in this design

The solution is two-fold:

  • split up things between the controller (Timer) and the worker (the closure)
  • share state between the two using Arc since references are forbidden

Here is a minimal example for you to toy with:

use std::{sync, thread, time};
use std::sync::atomic::{AtomicBool, Ordering};

pub struct Timer {
    handle: Option<thread::JoinHandle<()>>,
    alive: sync::Arc<AtomicBool>,
}

impl Timer {
    pub fn new() -> Timer {
        Timer {
            handle: None,
            alive: sync::Arc::new(AtomicBool::new(false)),
        }
    }

    pub fn start<F>(&mut self, fun: F)
        where F: 'static + Send + FnMut() -> ()
    {
        self.alive.store(true, Ordering::SeqCst);

        let alive = self.alive.clone();

        self.handle = Some(thread::spawn(move || {
            let mut fun = fun;
            while alive.load(Ordering::SeqCst) {
                fun();
                thread::sleep(time::Duration::from_millis(10));
            }
        }));
    }

    pub fn stop(&mut self) {
        self.alive.store(false, Ordering::SeqCst);
        self.handle
            .take().expect("Called stop on non-running thread")
            .join().expect("Could not join spawned thread");
    }
}

fn main() {
    let mut timer = Timer::new();
    timer.start(|| println!("Hello, World!") );

    println!("Feeling sleepy...");
    thread::sleep(time::Duration::from_millis(100));

    println!("Time for dinner!");
    timer.stop();
}

I invite you to poke holes at it one at a time (ie, change one thing that is different from your example, check the error message, and try to understand how the difference solved it).

On the playground, it printed for me:

Feeling sleepy...
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Time for dinner!

Though I would not rely on (1) the number of times "Hello, World!" appears and (2) "Feeling sleepy..." appearing first.

And damned, is Atomic verbose... I kinda wish there was a get/set with SeqCst (the stronger ordering) available.

Upvotes: 11

Related Questions