Async Isn't Real & Cannot Hurt You

Tris highlights the challenges async Rust poses to Rust’s core safety guarantees, particularly due to the 'static lifetime requirement in async runtimes like Tokio, which complicates borrowing and ownership compared to native threads. He recommends tightly scoping async tasks, using tools like Tokio’s scoped tasks or Rayon for parallelism, to maintain Rust’s safety principles and encourages developers to approach async Rust thoughtfully rather than viewing it as inherently problematic.

In this video, Tris shares his deep appreciation for Rust as a language that empowers developers to write both high-level application code and low-level driver code with confidence, thanks to features like the borrow checker, algebraic data types, powerful macros, and a friendly compiler. However, he highlights a significant challenge when it comes to async Rust: it seems to break Rust’s core promise of safe and easy borrowing. Async code in Rust often feels alien and is sometimes considered almost a different language because it complicates the ownership and lifetime model that Rust developers rely on.

Tris explains the difference between concurrency and parallelism, emphasizing that humans and single-threaded CPUs achieve concurrency by rapidly switching between tasks, creating the illusion of multitasking, while parallelism involves multiple tasks running simultaneously on multiple processors or threads. He points out that Rust’s strict lifetime and borrowing rules become a pain point when dealing with parallel async tasks, especially because async runtimes like Tokio require references to have a 'static lifetime, meaning they must be valid for the entire duration of the program, which is often impractical.

Using a practical example, Tris demonstrates how spawning async tasks with Tokio requires references to be 'static, which forces developers to loosen Rust’s borrowing guarantees and effectively disables much of the borrow checker’s power. This requirement contrasts with native OS threads, where Rust’s scoped threads allow borrowing references safely within a limited scope, ensuring threads complete before references are released. This scoped approach preserves Rust’s safety guarantees and feels more natural to Rust developers.

To address these challenges, Tris presents several solutions for writing async Rust code that better align with Rust’s principles. He ranks them from least to most preferred: using Arc for shared ownership, Tokio’s scoped tasks to limit lifetimes, the small crate which separates concurrency from parallelism and is lightweight, and finally Rayon for parallelism in synchronous code without introducing async complexity. He advocates for tightly scoping async code to maintain Rust’s compile-time guarantees and reduce reliance on runtime checks.

In conclusion, Tris reassures viewers that async Rust is not inherently problematic or “not real,” but it requires careful handling to avoid compromising Rust’s safety and clarity. He encourages developers to scope async tasks narrowly and consider alternatives like native threads or Rayon when possible. The video ends with an invitation to check out his new podcast, where he discusses Rust and related topics in more depth, reinforcing his commitment to helping the Rust community navigate these complexities.