2.5 Tokio
Tokio is a critical third-party library. It is an integral part of Deno. Deno is heavy on async code. Deno tries to avoid callbacks wherever possible. To make it possible, it takes Tokio's help in using equivalent async functions for commonly used sync functions.
Tokio is a runtime for writing reliable, asynchronous, and slim applications with the Rust programming language. It is an event-driven, non-blocking I/O platform for writing asynchronous applications in Rust. The non-blocking nature of Tokio is most useful for Deno's internal code.
Tokio is an asynchronous runtime and provides the building blocks needed for writing network applications. It gives the flexibility to target a wide range of systems, from large servers with dozens of cores to small embedded devices.
Tokio provides a lot of functionality. Here are some of the common ones:
- fs
- This module contains utility methods and adapter types for input/output to files or standard streams (Stdin, Stdout, Stderr), and filesystem manipulation)
- io
- This module is the asynchronous version of std::io
- net
- This module contains the TCP/UDP/Unix networking types, similar to the standard library, which can be used to implement networking protocols
- process
- asynchronous versions of functions that create processes
- runtime
- An I/O event loop, called the driver, which drives I/O resources and dispatches I/O events to tasks that depend on them
- A scheduler to execute tasks that use these I/O resources
- A timer for scheduling work to run after a set period of time
- task
- Asynchronous green-threads
- and many more
A common theme in the functionality is asynchronicity. Tokio tries to offer async equivalents of common sync/blocking functions. For example, tokio wraps an OS system call into an async function.
Let's see some of the advantages of using tokio:
Tokio's API is memory-safe, thread-safe, and misuse-resistant. This helps prevent common bugs, such as unbounded queues, buffer overflows, and task starvation.
Built on top of Rust, Tokio provides a multi-threaded, work-stealing scheduler. Applications can process hundreds of thousands of requests per second with minimal overhead.
async/await reduces the complexity of writing asynchronous applications. Paired with Tokio's utilities and vibrant ecosystem, writing applications is a breeze.
The needs of a server application differ from that of an embedded device. Although Tokio comes with defaults that work well out of the box, it also provides the knobs needed to fine-tune to different cases.
Tokio is the backbone of Deno. Without tokio, Deno won't be completely async. This book is about Deno, not Tokio. So, the discussion about tokio will be very limited. For a detailed description of tokio's capabilities, visit https://docs.rs/tokio/0.3.5/tokio/index.html.
But tokio is very important. We'll still go over the common Tokio functionality along with some code samples.
This module contains utility methods and adapter types for input/output to files or standard streams (Stdin, Stdout, Stderr), and filesystem manipulation, for use within (and only within) a Tokio runtime.
Here is the list of async functions provided by the async fs module:
- copy file
- create dir
- read dir
- remove dir
- read file
- write file
- remove file
- rename file
- set permissions
All the above functions are fully async. They don't block the thread. Here is a code sample that would help to understand better:
use tokio::fs;
use std::io;
#[tokio::main]
async fn main() -> io::Result<()> {
fs::create_dir("/some/dir").await?;
Ok(())
}
The above code will do an async read of /some/dir directory. This would be an async operation. The main thread won't be blocked. The await at the end of create_dir makes it async.
One more example of async fs operation:
use tokio::fs;
fs::write("foo.txt", b"Hello world!").await?;
The above code asynchronously writes Hello world to foo.txt.
This module is the asynchronous version of std::io. Primarily, it defines two traits, AsyncRead and AsyncWrite, which are asynchronous versions of the Read and Write traits in the standard library.
Here is the list of async functions provided by the async fs module:
- copy buffer
- stderr
- stdout
- stdin
- and some more
Here is an example of writing to stdout:
use tokio::io::{self, AsyncWriteExt};
#[tokio::main]
async fn main() -> io::Result<()> {
let mut stdout = io::stdout();
stdout.write_all(b"Hello world!").await?;
Ok(())
}
write_all will asynchronously write Hello World to stdout. As always, the main thread won't be blocked.
Here is another example involving buffers:
use tokio::io;
let mut reader: &[u8] = b"hello";
let mut writer: Vec<u8> = vec![];
io::copy_buf(&mut reader, &mut writer).await?;
assert_eq!(b"hello", &writer[..]);
copy_buf will copy from reader to write asynchronously.
This module contains the TCP/UDP/Unix networking types, similar to the standard library, which can be used to implement networking protocols.
Net has three sub-modules:
- TCP
- UDP
- Unix domain sockets
Here is an example of an asynchronous TCP client:
use tokio::net::TcpSocket;
use std::io;
#[tokio::main]
async fn main() -> io::Result<()> {
let addr = "127.0.0.1:8080".parse().unwrap();
let socket = TcpSocket::new_v4()?;
let stream = socket.connect(addr).await?;
Ok(())
}
The connect system call is a blocking one. But the connect() offered by Tokio is non-blocking. Connect will return when a connection is made. It won't block the main thread.
And here is an example of a UDP server:
use tokio::net::UdpSocket;
use std::io;
#[tokio::main]
async fn main() -> io::Result<()> {
let sock = UdpSocket::bind("0.0.0.0:8080").await?;
let mut buf = [0; 1024];
loop {
let (len, addr) = sock.recv_from(&mut buf).await?;
println!("{:?} bytes received from {:?}", len, addr);
let len = sock.send_to(&buf[..len], addr).await?;
println!("{:?} bytes sent", len);
}
}
There are three system calls here: bind, recv_from, send_to. All three system calls are in non-blocking mode.
This module provides a Command struct that imitates the interface of the std::process::Command type in the standard library but provides asynchronous versions of functions that create processes. These functions (spawn, status, output, and their variants) return "future aware" types that interoperate with Tokio.
This module offers the following async functionality:
- Create a child process
- Communication with child process (stdin, stdout, stderr)
The Command struct is useful in running commands like the following:
use tokio::process::Command;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// The usage is similar as with the standard library's `Command` type
let mut child = Command::new("echo")
.arg("hello")
.arg("world")
.spawn()
.expect("failed to spawn");
// Await until the command completes
let status = child.wait().await?;
println!("the command exited with: {}", status);
Ok(())
}
This is the tokio runtime.
Unlike other Rust programs, asynchronous applications require runtime support. In particular, the following runtime services are necessary:
- An I/O event loop, called the driver, which drives I/O resources and dispatches I/O events to tasks that depend on them.
- A scheduler to execute tasks that use these I/O resources.
- A timer for scheduling work to run after a set period of time.
Tokio's Runtime bundles all of these services as a single type, allowing them to be started, shut down, and configured together. However, often it is not required to configure a Runtime manually, and a user may just use the tokio::main attribute macro, which creates a Runtime under the hood.
In the Deno code, the tokio runtime is created using tokio::main attribute. This is enough to create a tokio runtime.
Tokio runtime might be a bit tough to understand. The main idea behind runtime is to provide support for async operations. Runtime is the backbone that dispatches and receives operations and results.
Tokio tasks are asynchronous green-threads.
A task is a lightweight, non-blocking unit of execution. A task is similar to an OS thread, but rather than being managed by the OS scheduler, they are managed by the Tokio runtime. Another name for this general pattern is green threads.
Tasks come with ther following functionality:
- Spawn a task
- Non-blocking task
- Spawn a task in blocking mode
- Sometimes blocking mode is required
- Block in place
- Blocks current thread in this place
Tasks are very efficient because:
- They are lightweight
- They are scheduled cooperatively
- They are non-blocking
Here are some examples of spawning a task in blocking, non-blocking mode. Also an example of block-in-place.
use tokio::task;
// NON-BLOCKING
let join = task::spawn(async {
// ...
"hello world!"
});
// ...
// Await the result of the spawned task.
let result = join.await?;
assert_eq!(result, "hello world!");
// BLOCKING
let join = task::spawn_blocking(|| {
// do some compute-heavy work or call synchronous code
"blocking completed"
});
let result = join.await?;
assert_eq!(result, "blocking completed");
// BLOCK IN PLACE
let result = task::block_in_place(|| {
// do some compute-heavy work or call synchronous code
"blocking completed"
});
assert_eq!(result, "blocking completed");
Deno uses tokio in many ways including:
- Spawning green threads for async ops
- Offering tokio's async functions to user space
Deno makes use of the tokio runtime with other async modules. Here is a code example from Deno code. The function is op_connect which gets called when there is a TCP connection required to be established.
async fn op_connect(
state: Rc<RefCell<OpState>>,
args: Value,
_zero_copy: BufVec,
) -> Result<Value, AnyError> {
// -- CODE OMITTED ---
let tcp_stream = TcpStream::connect(&addr).await?;
// -- CODE OMITTED ---
op_connect is an internal op used by Deno code. This function makes a TCP connection to the specified address. It is used by WebSocket which runs over TCP. As can be seen in the code, tokio offers TCP connect as an async operation.
let tcp_stream = TcpStream::connect(&addr).await?;
Tokio is a feature-rich and complicated library. It is very efficient and works like magic. This is just an introduction. More about tokio can be read here: https://tokio.rs/ and https://docs.rs/tokio/0.3.5/tokio/ (0.3.5 is the latest version at the time of writing).
Let's move on to another very complicated but very important third party library, V8 engine. Without v8, JS code can't run.
Last modified 2yr ago