consider a simple program that prints elapsed time after 5 seconds.
use std::time::Duration;
fn main() {
let timer_start = Instant::now();
thread::sleep(Duration::from_secs(5));
println!("elapsed: {:?}", timer_start.elapsed());
}
this will print something like:
elapsed: 5.000182958s
that tiny difference is because of thread scheduling which operating system does. let’s not worry about it for now.
now if you want to do something useful between this 5 seconds, you can’t. let’s modify the program so we can do that while we wait.
use std::time::{Duration, Instant};
use std::thread;
fn main() {
let timer_start = Instant::now();
let timer_duration = Duration::from_secs(5);
loop {
let elapsed = timer_start.elapsed();
if elapsed >= timer_duration {
println!("elapsed: {:?}", elapsed);
break;
}
println!("doing useful work...");
// sleep before checking again
// to prevent wasting cpu cycles
thread::sleep(Duration::from_secs(1));
}
}
this will print something like:
doing useful work...
doing useful work...
elapsed: 5.000748332s
note: not all prints for “doing useful work…” are shown below, just later ones.
this is fair. we are doing useful work while waiting. 1 second wait is used just for example, in real world, this wait time will be much less.
now notice as 5 seconds is multiple of 1 second. in real world, this is not necessary to be the case. consider a case where wait time is 2 seconds. it will print something like:
doing useful work...
doing useful work...
elapsed: 6.000440721s
here, we waited for whole 1 second more than needed. why is this a problem, in real world, this wait time will be much less anyways?
yes, but event we are waiting for might be critical like mouse click, or user typed something. even few milliseconds can make a difference. imagine when you press backspace and it deletes series of characters in one go.
to solve this, let’s modify the program to use event loop instead. here i’m using calloop
crate, but any event loop crate will do.
use calloop::{
timer::{TimeoutAction, Timer},
EventLoop, LoopSignal,
};
use std::time::{Duration, Instant};
fn main() {
let start_time = Instant::now();
let mut event_loop: EventLoop<LoopSignal> =
EventLoop::try_new().expect("failed to create event loop");
let handle = event_loop.handle();
let source = Timer::from_duration(Duration::from_secs(5));
handle
.insert_source(source, |_, _, shared_data| {
let elapsed = start_time.elapsed();
println!("elapsed: {:?}", elapsed);
// notify the event loop to stop
shared_data.stop();
// we don't want to reschedule the timer, so we just drop it
TimeoutAction::Drop
})
.expect("failed to insert source");
// this is just the shared data that will be passed to the event loop
// in this case it's just a signal to stop the event loop
let mut shared_data = event_loop.get_signal();
event_loop
.run(
// wait for most 2 seconds
Duration::from_secs(2),
&mut shared_data,
|_| {
// this runs after every 2 seconds or when the event fires
// whichever happens first
println!("doing useful work");
},
)
.expect("error during event loop");
}
this will print something like:
doing useful work
doing useful work
elapsed: 5.000179384s
doing useful work
tada! ** chime sound here **
here, under the hood, it uses epoll
on linux (different on mac and windows), which is operating system mechanism to notify about events. our callback will be called every 2 seconds if event is not yet fired, or immediately if event is fired.
using even loop you can even add multiple sources, if we do that using our previous approach, it would check every event each time, which is not efficient.
use std::time::{Duration, Instant};
use std::thread;
fn main() {
loop {
// here each time we check all events
check_some_event();
check_some_other_event();
check_third_event();
println!("doing useful work...");
thread::sleep(Duration::from_secs(1));
}
}
event loop doesn’t need scheduled checking, it will call callback right when event happens.
i will write more on internals of event loop and different types of event loops in later posts.