Timers
When writing a network based application, it is common to need to performactions based on time.
- Run some code after a set period of time.
- Cancel a running operation that takes too long.
- Repeatedly perform an action at an interval.
These use cases are handled by using the various timer APIs that are provided inthe timer module.
Running code after a period of time
In this case, we want to perform a task after a set period of time. To do this,we use the Delay
API. All we will do is write "Hello world!"
to theterminal, but any action can be taken at this point.
# #![deny(deprecated)]
# extern crate tokio;
#
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 100milliseconds in the future. The new
function takes an Instant
, so we computewhen
to be the instant 100 milliseconds from now.
Once the instant is reached, the Delay
future completes, resulting in theand_then
block to be executed.
As with all futures, Delay
is lazy. Simply creating a new Delay
instancedoes nothing. The instance must be used on a task that is spawned onto the Tokioruntime. The runtime comes preconfigured with a timer implementation todrive the Delay
instance to completion. In the example above, this is done bypassing the task to tokio::run
. Using tokio::spawn
would also work.
Timing out a long running operation
When writing robust networking applications, it’s critical to ensure thatoperations complete within reasonable amounts of time. This is especially truewhen waiting for data from external, potentially untrusted, sources.
The Timeout
type ensures that an operation completes by a specifiedinstant in time.
# #![deny(deprecated)]
# extern crate tokio;
#
use tokio::io;
use tokio::net::TcpStream;
use tokio::prelude::*;
use std::time::{Duration, Instant};
fn read_four_bytes(socket: TcpStream)
-> Box<Future<Item = (TcpStream, Vec<u8>), Error = ()> + Send>
{
let buf = vec![0; 4];
let fut = io::read_exact(socket, buf)
.timeout(Duration::from_secs(5))
.map_err(|_| println!("failed to read 4 bytes by timeout"));
Box::new(fut)
}
# pub fn main() {}
The above function takes a socket and returns a future that completes when 4bytes have been read from the socket. The read must complete within 5 seconds.This is ensured by calling timeout
on the read future with a duration of 5seconds.
The timeout
function is defined by FutureExt
and is included in theprelude. As such, use tokio::*
imports FutureExt
as well, sowe can call timeout
on all futures in order to require them to complete bythe specified instant.
If the timeout is reached without the read completing, the read operation isautomatically canceled. This happens when the future returned byio::read_exact
is dropped. Because of the lazy runtime model, dropping afuture results in the operation being canceled.
Running code on an interval
Repeatedly running code on an interval is useful for cases like sending a PINGmessage on a socket, or checking a configuration file every so often. This canbe implemented by repeatedly creating Delay
values. However, becausethis is a common pattern, Interval
is provided.
The Interval
type implements Stream
, yielding at the specified rate.
# #![deny(deprecated)]
# extern crate tokio;
#
use tokio::prelude::*;
use tokio::timer::Interval;
use std::time::{Duration, Instant};
fn main() {
let task = Interval::new(Instant::now(), Duration::from_millis(100))
.take(10)
.for_each(|instant| {
println!("fire; instant={:?}", instant);
Ok(())
})
.map_err(|e| panic!("interval errored; err={:?}", e));
tokio::run(task);
}
The above example creates an Interval
that yields every 100 millisecondsstarting now (the first argument is the instant at which the Interval
shouldfirst fire).
By default, an Instant
stream is unbounded, i.e., it will continue yielding atthe requested interval forever. The example uses Stream::take
to limit thenumber of times Interval
yields, here limiting to a sequence of 10 events.So, the example will run for 0.9 seconds since the first of 10 values is yieldedimmediately.
Notes on the timer
The Tokio timer has a granularity of one millisecond. Any smaller interval isrounded up to the nearest millisecond. The timer is implemented in user land(i.e., does not use an operating system timer like timerfd
on linux). It usesa hierarchical hashed timer wheel implementation, which provides efficientconstant time complexity when creating, canceling, and firing timeouts.
The Tokio runtime includes one timer instance per worker thread. This meansthat, if the runtime starts 4 worker threads, there will be 4 timerinstances. This allows avoiding synchronization in most cases since the task,when working with a timer, will be operating on state located on the currentthread.
That said, the timer implementation is thread safe and supports usage fromany thread.
Next up: Essential combinators