Async/Await: Asynchronous Programming

Many operations we ask the computer to perform can take a while to complete. For example, downloading a video from a web server involves waiting for network data to arrive, while exporting that video uses intensive CPU computation. These are fundamentally different kinds of waiting, and handling them efficiently is crucial for responsive, performant applications.

Async vs. Blocking

When you call a function that performs I/O (like reading a file or making a network request), you have two choices:

  1. Blocking: The function waits until the operation completes before returning. Simple, but your program can't do anything else while waiting.

  2. Asynchronous: The function returns immediately, giving you a "future" that will eventually contain the result. Your program can do other work while waiting.

// Blocking approach - program waits for download to complete
let data = downloadFile(url)  // Blocks until done
processData(data)

// Async approach - program can do other work while waiting
let future = downloadFileAsync(url)
doOtherWork()
let data = await future  // Wait for result when we need it
processData(data)

Concurrency vs. Parallelism

These terms are related but distinct:

  • Concurrency is about dealing with multiple things at once. A single chef managing multiple dishes, switching between them as needed.

  • Parallelism is about doing multiple things at once. Multiple chefs each working on their own dish simultaneously.

Async programming primarily enables concurrency: efficiently managing multiple tasks even on a single CPU core. Whether those tasks actually run in parallel depends on your hardware and runtime configuration.

When to Use Async

Async programming shines for I/O-bound operations:

  • Network requests (HTTP calls, database queries)
  • File system operations
  • User input handling
  • Timer-based events

For CPU-bound operations (heavy computation), traditional multithreading with std.thread might be more appropriate. However, many real-world applications mix both patterns.

Oxide's Async Model

Oxide provides async programming with a few key components:

  1. async fn: Declares a function that can be paused and resumed
  2. await: Waits for an async operation to complete (prefix syntax!)
  3. Futures: Values representing work that may complete in the future
  4. Runtimes: Execute async code (like Tokio or async-std)

Prefix Await: A Key Oxide Difference

Unlike Rust which uses postfix expr.await, Oxide uses prefix await:

// Oxide - prefix await (reads left-to-right)
let response = await fetch(url)
let data = await response.json()

Rust equivalent:

#![allow(unused)]
fn main() {
let response = fetch(url).await;
let data = response.json().await;
}

This syntax matches JavaScript, Python, Swift, and Kotlin, making async code read naturally from left to right.

Chapter Overview

In this chapter, we'll explore:

  1. Futures and Async Syntax - How to write async functions and understand futures
  2. Concurrency with Async - Running multiple async operations together
  3. Working with More Futures - Advanced patterns like racing and timeouts
  4. Streams - Processing sequences of async values
  5. Traits for Async - Understanding Future, Pin, and Stream traits

By the end of this chapter, you'll be comfortable writing async code in Oxide and understand how it differs from traditional blocking code.

A Note on Runtimes

Unlike languages with built-in runtimes (JavaScript, Python), Rust and Oxide require you to choose an async runtime. The most popular options are:

  • Tokio: Full-featured, production-ready runtime
  • async-std: Standard library-like API
  • smol: Lightweight, minimal runtime

This book uses Tokio in examples, but the concepts apply to any runtime. The runtime handles scheduling, executing, and coordinating your async code.

Let's dive in!