2.8 Tokio

Overview

Tokio, a crucial third-party library, plays an essential role within the framework of Deno. Deno heavily relies on asynchronous (async) programming. This approach aims to minimize the use of callbacks whenever feasible. To achieve this goal, Deno leverages Tokio, which assists in transforming commonly used synchronous (sync) functions into their asynchronous counterparts.

Tokio serves as a runtime environment tailored for crafting dependable, lean, and asynchronous applications using the Rust programming language. It functions as an event-driven, non-blocking platform for creating applications with asynchronous capabilities in Rust. Deno greatly benefits from Tokio's non-blocking characteristics, which align with the architecture of Deno's internal code.

Functioning as an asynchronous runtime, Tokio equips developers with the fundamental components necessary for crafting network-oriented applications. Its design empowers developers to target a wide array of systems, spanning from sizeable servers equipped with numerous 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.

A unifying theme woven throughout Tokio's functionality is its emphasis on asynchronicity. Tokio diligently strives to present asynchronous counterparts to conventional synchronous or blocking functions. To illustrate, Tokio encapsulates traditional OS system calls within asynchronous functions, aligning with its commitment to asynchronous programming principles. This intricate interplay of Tokio's capabilities fosters the synergy between Deno's async-heavy nature and Tokio's non-blocking prowess.

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

Reliable

Tokio's API ensures safety at multiple levels: memory, threads, and resistance to misuse. This comprehensive approach is crucial in thwarting various typical errors that can arise in programming. These errors include issues like unbounded queues, buffer overflows, and the problem of task starvation.

Starting with memory safety, Tokio's API is designed to carefully manage memory usage, minimizing the chances of memory-related bugs. This helps your program run smoothly and avoids crashes caused by memory leaks or access to invalid memory locations.

Thread safety is another key aspect of Tokio's API. It enables different parts of your program to execute concurrently without causing conflicts or race conditions. This means that even when multiple tasks are running at the same time, Tokio ensures that they don't interfere with each other's data, reducing the likelihood of unpredictable behavior and hard-to-debug issues.

Moreover, Tokio's API is built with a strong focus on preventing misuse. This involves setting up guardrails to guide developers away from practices that could lead to bugs or vulnerabilities. By providing clear guidelines and structures, Tokio helps developers avoid common pitfalls and follow best practices.

By combining these safety measures, Tokio not only empowers you to create efficient and responsive applications but also assists in writing robust and reliable code. It acts as a safety net, catching potential problems early in the development process, and ultimately contributes to a more stable and trustworthy software ecosystem.

Fast

Constructed using the Rust programming language, Tokio offers a sophisticated multi-threaded scheduler that employs a technique called work-stealing. This innovation allows applications to efficiently handle an impressive volume of requests, reaching up to hundreds of thousands per second. This remarkable capability is achieved while keeping any unnecessary additional processing to a minimum. Thanks to Tokio's architecture, developers can create high-performance applications that scale gracefully and deliver responsive user experiences. This makes Tokio a pivotal tool in enabling developers to craft powerful and efficient applications that effectively manage significant workloads.

Easy

Using async/await significantly simplifies the process of creating asynchronous applications. When combined with the helpful tools and dynamic environment offered by Tokio's ecosystem, the task of developing applications becomes incredibly straightforward and effortless. Tokio, provides a range of utilities and resources that seamlessly integrate with async/await. These utilities assist in handling tasks like asynchronous I/O operations, timers, and managing concurrent execution. With Tokio's support, you can focus more on your application's logic and features, rather than getting bogged down in managing low-level asynchronous intricacies.

Flexible

The requirements of a server application are not the same as those of an embedded device. While Tokio comes with preset configurations that function effectively from the start, it also offers the necessary tools to adjust and optimize for various scenarios.

Server applications involve managing a larger volume of incoming requests and need to handle them efficiently. These applications demand a higher level of performance and responsiveness. On the other hand, embedded devices operate with limited resources, such as memory and processing power, which means they require careful resource allocation to ensure smooth operation.

--

Tokio serves as the foundation of Deno's asynchronous functionality, playing a crucial role in enabling Deno to operate entirely in an asynchronous manner. Although this book primarily focuses on Deno, it's important to acknowledge the significance of Tokio in Deno's ecosystem.

It's worth noting that while this book delves into Deno's features, our discussion of Tokio will be limited to ensure we maintain our primary focus. If you're seeking an in-depth understanding of Tokio's capabilities, you can explore its comprehensive documentation at https://docs.rs/tokio/0.3.5/tokio/index.html.

Functionalities

FS

This tokio module plays an important role by offering convenient tools and adaptable structures for handling input and output with files or common streams like Stdin, Stdout, and Stderr. Additionally, it assists in managing the filesystem. These tools are designed specifically to be used within the context of a Tokio runtime environment.

Within the async fs module, you'll discover a collection of asynchronous functions that seamlessly handle various filesystem tasks. These functions are designed to work harmoniously within the asynchronous programming paradigm, ensuring that they don't halt the entire thread while performing their tasks. Here's a list of the provided async functions:

  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.

Each of the functions mentioned above operates asynchronously, meaning they are skillfully designed to function without causing any interruption to the overall program execution. This asynchronous behavior ensures that other tasks can continue to progress concurrently without being blocked.

To enhance your understanding, here's a simple code snippet that illustrates the usage of these async filesystem functions within the context of 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 provided code snippet performs an asynchronous read of the "/some/dir" directory. This action operates asynchronously, meaning it doesn't halt the main thread. The usage of the 'await' keyword at the conclusion of 'create_dir' designates it as an asynchronous operation.

By examining and experimenting with code like the above, you can gain practical insight into how these async filesystem functions function within the Tokio ecosystem, fostering a deeper comprehension of their capabilities and benefits.

One more example of async fs operation:

use tokio::fs;

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

The provided code accomplishes an asynchronous task by writing "Hello world" to the file named foo.txt.

IO

The tokio module plays the role of an asynchronous counterpart to std::io. Its main purpose revolves around introducing two key traits: AsyncRead and AsyncWrite. These traits serve as the asynchronous counterparts to the Read and Write traits found in the standard library.

When it comes to the async fs module, it brings a range of asynchronous functions into the mix. These functions include:

  • Copying buffers

  • Managing standard error (stderr)

  • Handling standard output (stdout)

  • Dealing with standard input (stdin)

  • And a few others as well

To illustrate, let's delve into an example of how to write content to the standard output (stdout). This showcases the practical application of the concepts discussed above:

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 allows you to write "Hello World" to the standard output (stdout) in an asynchronous manner. As usual, this action won't cause the main thread to become blocked or unresponsive.

Let's explore another example that demonstrates the use of 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[..]);

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

Net

This tokio module encompasses essential networking components like TCP, UDP, and Unix networking types. These components, akin to those found in the standard library, are crucial for developing various networking protocols.

Within the 'net' module, you'll discover three sub-modules, each catering to a specific networking aspect:

  1. TCP

  2. UDP

  3. Unix domain sockets

Let's delve into each of these sub-modules to better understand their functionalities:

  1. TCP: Transmission Control Protocol (TCP) is a fundamental building block of internet communication. It ensures reliable, ordered, and error-checked delivery of data between devices. The TCP sub-module in the 'net' module equips developers with tools to create and manage TCP connections, enabling the establishment of robust and secure communication channels.

  2. UDP: User Datagram Protocol (UDP) offers a faster but less reliable form of communication compared to TCP. It's commonly used for tasks where speed is crucial, and occasional data loss is acceptable. The UDP sub-module within the 'net' module facilitates the implementation of UDP-based communication, allowing developers to design applications that prioritize speed and efficiency.

  3. Unix Domain Sockets: Unix domain sockets provide a means for processes to communicate locally on the same machine. Unlike TCP or UDP, which operate over a network, Unix domain sockets offer inter-process communication (IPC) within the confines of a single system. The sub-module dedicated to Unix domain sockets in the 'net' module aids developers in setting up local communication channels between processes, enhancing efficiency for applications that require fast and secure data exchange.

To illustrate the practical usage of these networking capabilities, here's an example showcasing an asynchronous TCP client. This client demonstrates the utilization of the tools provided by the tokio module to establish a TCP connection asynchronously, enabling efficient and non-blocking communication between the client and a server. This approach enhances the overall responsiveness and scalability of applications that rely on 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 obstructs other processes until its task is complete. However, the connect() function provided by Tokio follows a non-blocking approach. When you use connect(), it will swiftly conclude and provide a result as soon as a connection is established. This means that your main program thread won't be held up or delayed.

To illustrate, consider the following instance 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);
    }
}

In this context, there exist three fundamental system calls: bind, recv_from, and send_to. It's worth noting that all of these three system calls operate in what's known as a non-blocking mode.

Process

The tokio module introduces a Command structure designed to mimic the std::process::Command type found in the standard library. However, it goes beyond by offering asynchronous variations of the functions responsible for process creation. These functions, namely spawn, status, output, and their related versions, yield types that are in tune with futures and seamlessly work together with Tokio's capabilities.

Within this module, you'll discover a range of async features at your disposal:

  1. Child Process Creation: Easily spawn child processes asynchronously, allowing for efficient multitasking.

  2. Communication with Child Processes: Enable interaction with child processes asynchronously, managing inputs and outputs (stdin, stdout, stderr) with smooth efficiency.

The Command structure emerges as a valuable tool for executing commands in scenarios like the following:

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 ...

This Command module within tokio equips you with a powerful async foundation to manage child processes and their communication effectively.

Here is another example:

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 within the Deno ecosystem. In contrast to typical Rust programs, those designed for asynchronous functionality necessitate the presence of runtime support. Specifically, the following runtime services emerge as indispensable:

  1. I/O Event Loop - The Driver: This dynamic I/O event loop, aptly named the driver, takes charge of propelling I/O resources into action. It meticulously dispatches I/O events to tasks that rely on them for seamless execution.

  2. Scheduler - Task Executor: A dedicated scheduler is employed to efficiently orchestrate tasks reliant on these I/O resources. This ensures tasks are executed in a well-organized manner, optimizing performance.

  3. Timer - Temporal Precision: A time-sensitive component plays a pivotal role in the runtime, allowing the precise scheduling of tasks to commence after predefined time intervals.

Tokio's comprehensive Runtime amalgamates these essential services into a unified entity. The advantage lies in its ability to initiate, conclude, and tailor the services as a cohesive whole. Yet, it's noteworthy that crafting a Runtime from scratch isn't always imperative. The tokio::main attribute macro steps in, discreetly crafting a Runtime behind the scenes, alleviating the need for manual configuration.

Within the realm of Deno, the tokio runtime materializes with simplicity through the utilization of the tokio::main attribute. A mere inclusion of this attribute is adequate to summon the tokio runtime into existence.

Comprehending the nuances of the Tokio runtime may seem daunting at first. However, the core concept driving the runtime is to furnish unwavering support for asynchronous operations. In essence, the runtime serves as the backbone, proficiently directing and receiving operations and their consequential outcomes. This foundational pillar ensures the seamless flow of asynchronous tasks, contributing to the robustness of Deno's operational framework.

Task

Tokio tasks serve as asynchronous green-threads. A task embodies a lithe, non-obstructive unit of execution. It bears resemblance to an operating system thread, yet instead of being overseen by the OS scheduler, Tokio tasks are under the watchful eye of the Tokio runtime. This overarching concept is frequently referred to as green threads, a term indicative of its eco-friendly, resource-conserving nature.

If you'd like to delve deeper into the realm of green threads, you can explore this enlightening source: https://en.wikipedia.org/wiki/Green_threads.

The world of tasks unfolds with a range of indispensable functionalities, such as:

  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 provide a tangible grasp of these concepts, here are some illustrative instances of task spawning in both blocking and non-blocking modes, along with a demonstration of the block-in-place concept. By acquainting yourself with these examples, you can gain a clearer understanding of the versatile capabilities that Tokio tasks bestow upon 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 various manners, primarily for two key purposes:

  1. Creating Green Threads for Asynchronous Operations: Deno employs tokio to generate green threads, which are specialized threads that handle asynchronous operations efficiently. These threads can perform tasks concurrently without the need for traditional multithreading complexities. This approach enhances 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 within their own code. This integration allows developers to harness the power of tokio's asynchronous runtime for their applications, benefiting from its capabilities like efficient event handling and I/O operations.

Additionally, Deno synergizes with the tokio runtime alongside other asynchronous modules. Below is an illustrative code snippet sourced from Deno's codebase. The highlighted function is called op_connect, invoked whenever the need arises to establish a TCP connection:

In this excerpt, Deno's integration with tokio shines through the op_connect function, showcasing how Deno capitalizes on tokio's capabilities to manage asynchronous operations efficiently, especially in scenarios involving TCP connections. This collaboration 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 serves as an internal operation employed within Deno's code structure. This particular function facilitates the establishment of a TCP connection directed towards a designated address. This functionality becomes particularly relevant in scenarios involving WebSocket operations, which, in turn, rely on the underlying TCP protocol for their functioning.

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

--

Tokio stands out as a multifaceted and intricate library, boasting a plethora of features. Its efficiency is nothing short of magical, enabling remarkable functionality. However, let's view this as merely an initial glimpse into its capabilities. For those intrigued to delve deeper, an extensive resource awaits at: https://tokio.rs/ along with additional insights available at https://docs.rs/tokio/latest/tokio/. (As of the time of composing, version 1.32.0 stands as the latest iteration).

Shifting our focus, we encounter another pivotal third-party library known as the V8 engine. Despite its intricacy, its significance cannot be overstated. This engine serves as the backbone for executing JavaScript code, underscoring its indispensable role in this realm.

Last updated