New Timer implementation
March 30, 2018
Happy Friday all!
To close out a great week, there is a new release of Tokio. This release includes a brand new timer implementation.
Timers
Sometimes (often), one wants to execute code in relation to time. Maybe a function needs to run at a specific instant. Maybe a read needs to be limited to a fixed duration. For working with time, one needs access to a timer!
Some history
The tokio-timer
crate has been around for a while. It was originally built
using a hashed timer wheel (pdf warning). It had a granularity of 100
milliseconds, so any timeout set with a resolution of less than 100 milliseconds
would get rounded up. Usually, in the context of network based applications,
this is fine. Timeouts are usually at least 30 seconds and do not require high
precision.
However, there are cases for which 100 milliseconds is too coarse. Also, the original implementation of Tokio timer had a number of annoying bugs and did not handle edge cases super well due to the implementation strategy it took.
A new beginning
The timer has been rewritten from scratch and released as tokio-timer
0.2. For the most part, the API is pretty similar, but implementation is
completely different.
Instead of just using a single hashed timer wheel implementation, it uses a hierarchical approach (also described in the paper linked above).
The timer uses six separate levels. Each level is a hashed wheel containing 64 slots. Slots in the lowest level represent one millisecond. The next level up represents 64 milliseconds (1 x 64 slots) and so on. So, a slot on each level covers an equal amount of time as the entire level below.
When a timeout is set, if it is within 64 milliseconds from the current instant, it goes in the lowest level. If the timeout is within 64 milliseconds and 4,096 milliseconds, it goes in the second level, and so on.
As time advances, timeouts in the lowest level are fired. Once the end of the lowest level is reached, all timeouts in the next level up are removed from that level and moved to the lowest level.
Using this strategy, all timer operations (creating a timeout, canceling a timeout, firing a timeout) are constant. This results in very good performance even with a very large number of outstanding timeouts.
A quick look at the API.
As mentioned above, the API has not really changed. There are three primary types:
Delay
: A future that completes at a set instant in time.Timeout
: Decorates a future ensuring it completes before the timeout is reached.Interval
: A stream that yields values at a fixed intervals.
And a quick example:
use tokio::prelude::*;
use tokio::timer::Delay;
use std::time::{Duration, Instant};
fn main() {
let when = Instant::now() + Duration::from_millis(100);
let task = Delay::new(when)
.and_then(|_| {
println!("Hello world!");
Ok(())
})
.map_err(|e| panic!("delay errored; err={:?}", e));
tokio::run(task);
}
The above example creates a new Delay
instance that will complete 100
milliseconds in the future. The new
function takes an Instant
, so we compute
when
to be the instant 100 milliseconds from now.
Once the instant is reached, the Delay
future completes, resulting in the
and_then
block to be executed.
This release comes with a short guide explaining how to use timers and API documentation.
Integrated in the Runtime
Using the timer API requires a timer instance to be running. The Tokio runtime takes care of all that setup for you.
When the runtime is started with tokio::run
or by calling Runtime::new
directly, a thread pool is started. Each worker thread will get one timer
instance. So, this means that if the runtime starts 4 worker threads, there will
be 4 timer instances, one per thread. Doing this allows using the timer without
paying a synchronization cost since the timer will be located on the same thread
as the code that uses the various timer types (Delay
, Timeout
, Interval
).
And with that, have a great weekend!