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:
-
Blocking: The function waits until the operation completes before returning. Simple, but your program can't do anything else while waiting.
-
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:
async fn: Declares a function that can be paused and resumedawait: Waits for an async operation to complete (prefix syntax!)- Futures: Values representing work that may complete in the future
- 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:
- Futures and Async Syntax - How to write async functions and understand futures
- Concurrency with Async - Running multiple async operations together
- Working with More Futures - Advanced patterns like racing and timeouts
- Streams - Processing sequences of async values
- 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!