Hi folks. I am in my third or fourth week of learning Rust (just on weekends), and so far I been going through the official Rust book. My background is primarily working in higher-level languages (Scala, Python, Java, Objective-C). My reason for learning Rust is not so much real systems programming. I work at a higher abstraction writing services and applications. However, I want a language where I can write code once and interoperate from Java and Obj-C. Rust seems to fit the bill, and it seems to prevent me from shooting myself in the face (coughs in C++).
Ask
I am seeking feedback on the code (further below) as it's written now. I'm also seeking feedback on how to complete this implementation (it is not yet complete).
Requirements
I want to write a debouncer function. The API should be something like this:
// Initializer – create the debouncer
Debouncer(val timeout_in_milliseconds) => Debouncer
// API to call a function
void execute(val callbackFunction);
// API to gracefully shutdown
void terminate();
I want the semantics to be as follows. I can call the execute function any number of times. If a subsequent call occurs within the timeout window at time t
, then the debouncer should wait to execute the callbackFunction
until t + timeout_in_milliseconds
.
Random Thoughts
I'm really liking the message passing abstraction provided by Rust out of the box. I wrote a version of this in C++ last week, and now I'm trying to do it in Rust on top of the mpsc functionality. I'm also starting to understand the borrower, although not fully just yet.
Implementation
This implementation has the guts of a debouncer, but it's not fully there yet. The general idea is as follows –
- I am using the "new" pattern to construct and return an instance, i.e.
Debouncer::new()
. - The instance spawns a new thread. This thread listens for a
Signal
to execute some function,Signal::Execute
or to gracefully shutdown,Signal::Terminate
. - Once
Signal::Execute
is received via MPSC, a separate receiver uses therecv_timeout
API to block until someTimeout
duration. This receiver listens for message (just an integer), which indicates another call to the debouncer was made – meaning we want to "debounce" or essentially reset the clock and then execute. - The
Err
match forrecv_timeout
simply means that we timed out waiting for a subsequent call, meaning we're good to go ahead and execute whatever logic that needed to be debounced.
use std::time::{Duration, Instant};
use std::sync::mpsc;
use std::thread;
enum Signal {
Execute,
Terminate
}
struct Debouncer {
timeout: Duration,
last_invoked: Option<Instant>,
signal_sender: mpsc::Sender<Signal>,
debounce_sender: mpsc::Sender<u32>,
thread: Option<thread::JoinHandle<()>>
}
impl Debouncer {
pub fn new(timeout: Duration) -> Self {
let (signal_sender, signal_receiver) = mpsc::channel();
let (debounce_sender, debounce_receiver) = mpsc::channel();
let thread = thread::spawn(move || 'outer: loop {
let signal = signal_receiver.recv().unwrap();
'inner: loop {
let debounce = debounce_receiver.recv_timeout(timeout);
match debounce {
Ok(_) => {
println!("[Debouncing]");
},
Err(_) => {
match signal {
Signal::Execute => {
println!("[Executing]");
break 'inner;
},
Signal::Terminate => {
break 'outer;
}
}
}
}
}
});
Debouncer {
timeout,
last_invoked: None,
signal_sender,
debounce_sender,
thread: Some(thread)
}
}
pub fn execute<T>(&mut self, task: T) where T: FnOnce() + Send + 'static {
let now = Instant::now();
if self.last_invoked.is_none() {
self.signal_sender.send(Signal::Execute).unwrap();
} else {
if now.duration_since(self.last_invoked.unwrap()) > self.timeout {
self.signal_sender.send(Signal::Execute).unwrap();
} else {
self.debounce_sender.send(1).unwrap();
}
}
self.last_invoked = Some(now);
}
pub fn terminate(self) {
self.signal_sender.send(Signal::Terminate).unwrap();
if let Some(thread) = self.thread {
let result = thread.join();
match result {
Ok(_) => println!("Debouncer thread joined."),
Err(_) => println!("Debouncer thread could not join.")
}
}
}
}
fn main() {
let mut debouncer = Debouncer::new(Duration::from_millis(1000));
for _ in 0..5 {
thread::sleep(Duration::from_millis(500));
debouncer.execute(|| {
println!("Hello world!");
});
}
debouncer.terminate();
}
As you can see, this is not complete. The closure that's passed into the execute
API is not actually called.
The pattern I think I need to implement is something like this:
- The debouncer needs to have a "task" property in its struct that's something like a
Arc<Mutex<T>>
. This is essentially the closure. - On calls to
execute
, the task property should be updated. - Meanwhile, the spawned thread should have a reference to this same property. When it's ready to execute the code, it will invoke
t()
.
The key thing is that this task
property needs to be mutable, as it will change each time the execute
function is called.
Is there a more idiomatic approach?