2.8 Tokio

Overview

Tokio, a vital third-party library, plays a central role in Deno's framework. Deno relies heavily on asynchronous programming to minimize callbacks. To achieve this, Deno utilizes Tokio, which converts synchronous functions into asynchronous ones.

Tokio is a runtime environment for building reliable, efficient, and asynchronous applications in Rust. It provides an event-driven, non-blocking platform for creating asynchronous applications in Rust. Deno benefits from Tokio's non-blocking nature, aligning with its internal architecture.

As an asynchronous runtime, Tokio provides developers with the essential tools to build network-oriented applications. Its design enables developers to target a wide range of systems, from large servers with multiple cores to compact embedded devices.

The functionality provided by Tokio is extensive and versatile. Here are some of the notable features it offers:

  • fs: This module encompasses utility methods and adapter types tailored for input/output operations involving files and standard streams (such as Stdin, Stdout, and Stderr). It also facilitates filesystem manipulation.

  • io: Serving as the asynchronous counterpart to std::io, this module handles input/output tasks asynchronously.

  • net: Within this module, you'll find TCP, UDP, and Unix networking types. These types, similar to those present in the standard library, enable the implementation of diverse networking protocols.

  • process: This module presents asynchronous versions of functions responsible for creating processes.

  • runtime: The core of Tokio involves an I/O event loop, aptly referred to as the driver. This component manages I/O resources and distributes corresponding I/O events to tasks reliant on them. Furthermore, Tokio includes a scheduler designed to execute tasks that harness these I/O resources, along with a timer mechanism for scheduling tasks to commence after specific time intervals.

  • task: Encompassing asynchronous green-threads, this module aids in managing concurrent tasks.

  • and many more: Tokio's offerings extend beyond the aforementioned modules, encompassing a diverse array of functionalities.

Tokio's core functionality is built around asynchronicity, a unifying theme that runs throughout its features. It provides asynchronous versions of traditional synchronous functions, ensuring non-blocking operations. For instance, Tokio wraps OS system calls in asynchronous functions, adhering to its asynchronous programming approach. This integration harmonizes Deno's async-oriented design with Tokio's non-blocking capabilities, leading to a synergistic relationship that enhances overall performance.

Now, let's explore several benefits that arise from utilizing Tokio within the context of Deno programming:

Reliable

Tokio's API prioritizes safety across three key areas: memory, threads, and preventing misuse. This multi-faceted approach helps prevent common programming errors, including unbounded queues, buffer overflows, and task starvation.

Tokio's API ensures memory safety by carefully managing memory usage, reducing the likelihood of memory-related bugs and crashes. It also enables thread safety, allowing concurrent program execution without conflicts or race conditions. This means that multiple tasks can run simultaneously without interfering with each other's data, reducing unpredictable behavior and debugging issues.

Additionally, Tokio's API is designed to prevent misuse by providing guidelines and structures that guide developers away from potentially harmful practices. By combining these safety measures, Tokio enables the creation of efficient, responsive, robust, and reliable applications, acting as a safety net to catch potential problems early in development and contributing to a more stable software ecosystem.

Fast

Built with Rust, Tokio features a advanced multi-threaded scheduler that utilizes a work-stealing technique. This technology enables applications to handle a large volume of requests efficiently, processing up to hundreds of thousands per second. Notably, this performance is achieved with minimal additional processing overhead. Tokio's architecture empowers developers to create high-performance applications that scale efficiently and provide responsive user experiences. As a result, Tokio is a crucial tool for developers to build powerful and efficient applications that manage significant workloads effectively.

Easy

Using async/await makes building asynchronous applications much easier. With Tokio's ecosystem, which offers a dynamic environment and helpful tools, developing applications becomes very straightforward and effortless. Tokio provides various utilities and resources that work well with async/await, helping with tasks like asynchronous I/O operations, timers, and concurrent execution. With Tokio's support, you can focus on your application's logic and features, rather than worrying about the complexities of asynchronous programming. This allows you to develop applications more efficiently and effectively, without getting bogged down in low-level details.

Flexible

Tokio recognizes that server applications and embedded devices have different requirements. While Tokio provides preset configurations that work well out of the box, it also offers tools to customize and optimize for specific use cases. Server applications require efficient handling of a high volume of incoming requests, necessitating high performance and responsiveness. In contrast, embedded devices have limited resources, such as memory and processing power, requiring careful resource management to ensure optimal operation. Tokio accommodates these diverse needs, enabling developers to tailor their applications accordingly.

--

Tokio is the foundation of Deno's asynchronous functionality, enabling Deno to run entirely asynchronously. While this book focuses on Deno, it's important to recognize Tokio's significance in the Deno ecosystem. Although we'll discuss Deno's features in this book, our coverage of Tokio will be brief to maintain our focus. If you want to learn more about Tokio's capabilities, you can find comprehensive documentation at https://docs.rs/tokio/0.3.5/tokio/index.html.

Functionalities

FS

This Tokio module provides essential tools and flexible structures for handling input/output operations with files and standard streams like Stdin, Stdout, and Stderr. It also helps manage the filesystem. These tools are designed for use within the Tokio runtime environment.

The async fs module offers a collection of asynchronous functions that efficiently handle various filesystem tasks. These functions are designed for asynchronous programming, ensuring non-blocking execution and preventing thread halting. The following asynchronous functions are available:

  1. Copy File: This function facilitates the copying of a file from one location to another.

  2. Create Directory: This function allows you to create a new directory, aiding in organizing your filesystem.

  3. Read Directory: With this function, you can access the contents of a directory, enabling you to gather information about the files it contains.

  4. Remove Directory: When you need to delete a directory and its contents, this function comes to your rescue.

  5. Read File: Utilize this function to read the contents of a file asynchronously, avoiding any disruption to the program's flow.

  6. Write File: If you wish to write data to a file, this function ensures that the process occurs smoothly within the async environment.

  7. Remove File: This function handles the deletion of a specific file in an asynchronous manner.

  8. Rename File: When the need arises to rename a file, this function provides a seamless solution.

  9. Set Permissions: With this function, you can set the permissions for a file or directory according to your requirements.

The functions mentioned above are asynchronous, meaning they are designed to operate without interrupting the program's execution. This allows other tasks to continue running concurrently without being blocked.

To help illustrate their usage, here's a simple code example that demonstrates how to use these async filesystem functions within a Tokio runtime:

use tokio::fs;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Example usage of async filesystem functions
    let content = fs::read_to_string("input.txt").await?;
    println!("Content of the file: {}", content);

    fs::write("output.txt", "Hello, Tokio!").await?;

    Ok(())
}

Here is another one:

use tokio::fs;
use std::io;

#[tokio::main]
async fn main() -> io::Result<()> {
    fs::create_dir("/some/dir").await?;
    Ok(())
}

The code snippet reads the "/some/dir" directory asynchronously, without blocking the main thread. The use of 'await' at the end of 'create_dir' indicates that it's an asynchronous operation.

By studying and experimenting with code like this, you can gain hands-on understanding of how async filesystem functions work within the Tokio ecosystem, deepening your understanding of their capabilities and benefits.

Here's another example of an async fs operation:

use tokio::fs;

fs::write("foo.txt", b"Hello world!").await?;

The code writes "Hello world" to a file named foo.txt asynchronously.

IO

This Tokio module serves as an asynchronous version of std::io, introducing two key traits: AsyncRead and AsyncWrite. These traits are the asynchronous equivalents of the Read and Write traits in the standard library.

The async fs module provides various asynchronous functions, including:

  • Buffer copying

  • Standard error (stderr) management

  • Standard output (stdout) handling

  • Standard input (stdin) handling

  • And others

Let's explore an example of writing content to standard output (stdout), demonstrating the practical application of these concepts:

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(())
}

The write_all function enables asynchronous writing of "Hello World" to the standard output (stdout), without blocking the main thread.

Next, let's examine an example that demonstrates buffer usage:

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[..]);

The copy_buf interface allows for asynchronous copying from a reader to a writer.

Net

This Tokio module includes essential networking components, such as TCP, UDP, and Unix networking types. These components are crucial for developing various networking protocols, including those used in internet communication, local network communication, and more.

The 'net' module contains three sub-modules, each focused on a specific networking aspect:

  • TCP: Ensures reliable, ordered, and error-checked data delivery between devices, making it a fundamental building block of internet communication.

  • UDP: Offers faster but less reliable communication, suitable for tasks where speed is crucial and occasional data loss is acceptable.

  • Unix Domain Sockets: Enables local inter-process communication (IPC) on the same machine, providing a means for processes to communicate locally.

These sub-modules provide developers with tools to create and manage connections, establish communication channels, and design applications that prioritize speed, efficiency, and reliability.

An example of an asynchronous TCP client demonstrates how to use Tokio's tools to establish a TCP connection efficiently, enabling non-blocking communication between the client and server and enhancing overall responsiveness and scalability in networking interactions.

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 operates in a way that blocks other processes until its task is complete, hindering the progress of other tasks. In contrast, the connect() function provided by Tokio follows a non-blocking approach, enabling efficient and concurrent processing. When you use connect(), it swiftly concludes and provides a result as soon as a connection is established, without delaying your main program thread. This ensures that your program continues executing other tasks without interruption, improving overall performance and responsiveness.

To illustrate this concept, consider the following example of a UDP server, which demonstrates the benefits of Tokio's non-blocking connect() function:

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);
    }
}

In this context, three essential system calls are utilized: bind, recv_from, and send_to. It's important to recognize that all three of these system calls function in a non-blocking mode, enabling efficient and concurrent processing. This means that these system calls will not obstruct other processes, allowing for seamless execution and improved performance.

Process

The Tokio module introduces a Command structure, inspired by the std::process::Command type in the standard library, but with enhanced asynchronous capabilities for process creation. The module offers asynchronous variations of spawn, status, output, and related functions, which yield types compatible with futures and seamlessly integrate with Tokio's features.

This module provides a range of asynchronous features, including:

  • Child Process Creation: Asynchronous spawning of child processes for efficient multitasking.

  • Communication with Child Processes: Asynchronous interaction with child processes, managing inputs and outputs (stdin, stdout, stderr) with ease.

The Command structure proves valuable for executing commands in various scenarios, such as:

let mut cmd = Command::new("example_command");
cmd.arg("arg1").arg("arg2");

// Asynchronously spawn the child process
let child = cmd.spawn().await.expect("Failed to spawn the process");

// Communicate with the child process's stdin, stdout, and stderr asynchronously
let child_stdin = child.stdin().expect("Failed to open stdin");
let child_stdout = child.stdout().expect("Failed to open stdout");
let child_stderr = child.stderr().expect("Failed to open stderr");

// ... Further interactions with the child process ...

The Command module in Tokio provides a robust asynchronous foundation for efficiently managing child processes and their communication. This enables effective process management and seamless interaction with child processes.

Consider the following example, which demonstrates the module's capabilities:

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(())
}

Runtime

The Tokio runtime is a vital component in the Deno ecosystem, providing essential services for asynchronous program execution. Unlike typical Rust programs, asynchronous applications require runtime support for efficient operation.

The Tokio runtime offers the following vital services:

  • I/O Event Loop (Driver): A dynamic loop managing I/O resources and dispatching events to dependent tasks for seamless execution.

  • Scheduler (Task Executor): A dedicated scheduler efficiently orchestrating tasks relying on I/O resources, ensuring organized execution and optimal performance.

  • Timer (Temporal Precision): A time-sensitive component enabling precise task scheduling at predefined intervals.

Tokio's comprehensive Runtime integrates these services into a unified entity, allowing for initiation, conclusion, and customization as a cohesive whole. Notably, manual runtime creation is not always necessary, as the tokio::main attribute macro automatically generates a Runtime, simplifying the process. This convenience feature eliminates the need for manual configuration, making it easier to utilize the Tokio runtime.

In Deno, the Tokio runtime is easily accessible via the tokio::main attribute, streamlining asynchronous operations. While understanding the Tokio runtime may seem complex initially, its core concept is to provide unwavering support for asynchronous tasks, serving as the backbone for efficient operation and outcome management. By leveraging the Tokio runtime, developers can create scalable and efficient asynchronous applications with ease.

Task

Tokio tasks are asynchronous green-threads, representing a lightweight and non-obstructive unit of execution. Similar to operating system threads, tasks execute independently, but unlike OS threads, they are managed by the Tokio runtime instead of the OS scheduler. This concept is commonly known as green threads, a term that highlights their resource-efficient and eco-friendly nature.

For a more in-depth exploration of green threads, you can visit this informative resource: https://en.wikipedia.org/wiki/Green_threads.

The realm of tasks offers a range of essential functionalities, including:

  1. Task Spawning: Initiating a new task's lifecycle.

  2. Non-Blocking Tasks: Tasks that execute without causing obstructions.

  3. Blocking Mode Task Spawning: Creating tasks in a mode that allows for potential blocks.

  4. Selective Blocking Mode: Instances where utilizing a blocking mode is warranted.

  5. Block-in-Place: Temporarily halting the ongoing operations of the present thread at a specific juncture.

The underpinning of tasks rests upon their exceptional efficiency, attributable to several factors:

  1. Lightweight Nature: Tasks are notably gentle on system resources.

  2. Cooperative Scheduling: Task scheduling is orchestrated in a cooperative manner.

  3. Non-Blocking Essence: The inherent design of tasks ensures they operate without inducing blocks.

To help you understand these concepts better, let's consider some examples of task spawning in both blocking and non-blocking modes, as well as a demonstration of the block-in-place concept. By reviewing these examples, you will gain a better understanding of the flexibility and capabilities that Tokio tasks offer in Deno applications.

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's usage

Deno utilizes Tokio in two main ways:

  1. Creating green threads for asynchronous operations: Deno uses Tokio to create green threads, specialized threads that handle asynchronous operations efficiently. These threads enable concurrent task execution without the complexities of traditional multithreading, enhancing Deno's ability to manage multiple asynchronous tasks smoothly.

  2. Extending Tokio's async capabilities to user space: Deno provides users with access to Tokio's asynchronous functions, allowing developers to leverage Tokio's asynchronous runtime capabilities, such as efficient event handling and I/O operations, in their applications.

Deno also integrates with the Tokio runtime and other asynchronous modules. The following code snippet from Deno's codebase illustrates this integration:

The highlighted op_connect function demonstrates how Deno utilizes Tokio's capabilities to manage asynchronous operations efficiently, particularly in establishing TCP connections. This integration enables Deno to provide a robust and performant environment for handling various network-related tasks.

pub async fn op_net_connect_tcp<NP>(
  state: Rc<RefCell<OpState>>,
  addr: IpAddr,
) -> Result<(ResourceId, IpAddr, IpAddr), AnyError>
where
  NP: NetPermissions + 'static,
{
  {
    let mut state_ = state.borrow_mut();
    state_
      .borrow_mut::<NP>()
      .check_net(&(&addr.hostname, Some(addr.port)), "Deno.connect()")?;
  }

  let addr = resolve_addr(&addr.hostname, addr.port)
    .await?
    .next()
    .ok_or_else(|| generic_error("No resolved address found"))?;
  let tcp_stream = TcpStream::connect(&addr).await?;
  let local_addr = tcp_stream.local_addr()?;
  let remote_addr = tcp_stream.peer_addr()?;

  let mut state_ = state.borrow_mut();
  let rid = state_
    .resource_table
    .add(TcpStreamResource::new(tcp_stream.into_split()));

  Ok((rid, IpAddr::from(local_addr), IpAddr::from(remote_addr)))
}

The op_net_connect function is an internal operation used within Deno's codebase. It enables the establishment of a TCP connection to a specified address. This functionality is crucial in scenarios involving WebSocket operations, which rely on the underlying TCP protocol for their operation.

let tcp_stream = TcpStream::connect(&addr).await?;

--

In summary, Tokio is a complex and multifaceted library with numerous features. Its efficiency is remarkable, enabling exceptional functionality. This is just a brief introduction to its capabilities. For further exploration, extensive resources are available at https://tokio.rs/ and https://docs.rs/tokio/latest/tokio/ (currently version 1.38.0).

Next, we have the V8 engine, a crucial third-party library that plays a vital role in executing JavaScript code. Despite its complexity, its importance cannot be overstated. The V8 engine serves as the foundation for JavaScript execution, highlighting its essential role in this context.

Last updated