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!

—Carl Lerche