Why Async?
We all love how Rust empowers us to write fast, safe software.But how does asynchronous programming fit into this vision?
Asynchronous programming, or async for short, is aconcurrent programming modelsupported by an increasing number of programming languages.It lets you run a large number of concurrenttasks on a small number of OS threads, while preserving much of thelook and feel of ordinary synchronous programming, through theasync/await
syntax.
Async vs other concurrency models
Concurrent programming is less mature and "standardized" thanregular, sequential programming. As a result, we express concurrencydifferently depending on which concurrent programming modelthe language is supporting.A brief overview of the most popular concurrency models can helpyou understand how asynchronous programming fits within the broaderfield of concurrent programming:
- OS threads don't require any changes to the programming model,which makes it very easy to express concurrency. However, synchronizingbetween threads can be difficult, and the performance overhead is large.Thread pools can mitigate some of these costs, but not enough to supportmassive IO-bound workloads.
- Event-driven programming, in conjunction withcallbacks, can be veryperformant, but tends to result in a verbose, "non-linear" control flow.Data flow and error propagation is often hard to follow.
- Coroutines, like threads, don't require changes to the programming model,which makes them easy to use. Like async, they can also support a largenumber of tasks. However, they abstract away low-level details thatare important for systems programming and custom runtime implementors.
- The actor model divides all concurrent computation into units calledactors, which communicate through fallible message passing, much likein distributed systems. The actor model can be efficiently implemented, but it leavesmany practical issues unanswered, such as flow control and retry logic.
In summary, asynchronous programming allows highly performant implementationsthat are suitable for low-level languages like Rust, while providingmost of the ergonomic benefits of threads and coroutines.
Async in Rust vs other languages
Although asynchronous programming is supported in many languages, somedetails vary across implementations. Rust's implementation of asyncdiffers from most languages in a few ways:
- Futures are inert in Rust and make progress only when polled. Dropping afuture stops it from making further progress.
- Async is zero-cost in Rust, which means that you only pay for what you use.Specifically, you can use async without heap allocations and dynamic dispatch,which is great for performance!This also lets you use async in constrained environments, such as embedded systems.
- No built-in runtime is provided by Rust. Instead, runtimes are provided bycommunity maintained crates.
- Both single- and multithreaded runtimes are available in Rust, which havedifferent strengths and weaknesses.
Async vs threads in Rust
The primary alternative to async in Rust is using OS threads, eitherdirectly throughstd::thread
or indirectly through a thread pool.Migrating from threads to async or vice versatypically requires major refactoring work, both in terms of implementation and(if you are building a library) any exposed public interfaces. As such,picking the model that suits your needs early can save a lot of development time.
OS threads are suitable for a small number of tasks, since threads come withCPU and memory overhead. Spawning and switching between threadsis quite expensive as even idle threads consume system resources.A thread pool library can help mitigate some of these costs, but not all.However, threads let you reuse existing synchronous code without significantcode changes—no particular programming model is required.In some operating systems, you can also change the priority of a thread,which is useful for drivers and other latency sensitive applications.
Async provides significantly reduced CPU and memoryoverhead, especially for workloads with alarge amount of IO-bound tasks, such as servers and databases.All else equal, you can have orders of magnitude more tasks than OS threads,because an async runtime uses a small amount of (expensive) threads to handlea large amount of (cheap) tasks.However, async Rust results in larger binary blobs due to the statemachines generated from async functions and since each executablebundles an async runtime.
On a last note, asynchronous programming is notbetter than threads,but different.If you don't need async for performance reasons, threads can often bethe simpler alternative.
Example: Concurrent downloading
In this example our goal is to download two web pages concurrently.In a typical threaded application we need to spawn threadsto achieve concurrency:
fn get_two_sites() { // Spawn two threads to do work. let thread_one = thread::spawn(|| download("https://www.foo.com")); let thread_two = thread::spawn(|| download("https://www.bar.com")); // Wait for both threads to complete. thread_one.join().expect("thread one panicked"); thread_two.join().expect("thread two panicked");}
However, downloading a web page is a small task; creating a threadfor such a small amount of work is quite wasteful. For a larger application, itcan easily become a bottleneck. In async Rust, we can run these tasksconcurrently without extra threads:
async fn get_two_sites_async() { // Create two different "futures" which, when run to completion, // will asynchronously download the webpages. let future_one = download_async("https://www.foo.com"); let future_two = download_async("https://www.bar.com"); // Run both futures to completion at the same time. join!(future_one, future_two);}
Here, no extra threads are created. Additionally, all function calls are staticallydispatched, and there are no heap allocations!However, we need to write the code to be asynchronous in the first place,which this book will help you achieve.
Custom concurrency models in Rust
On a last note, Rust doesn't force you to choose between threads and async.You can use both models within the same application, which can beuseful when you have mixed threaded and async dependencies.In fact, you can even use a different concurrency model altogether,such as event-driven programming, as long as you find a library thatimplements it.