Async is all the rage. Python, Rust, Go. Pick your language and chances are it’s got some form of async / await going. If you’ve never worked with async / await before this can all be somewhat confusing, to put it mildly. In this post we’ll take a high level overview of asynchronous programming in general before making our way to Rust, where we’ll look at the current state of async—what it is, how to do it, and how it all works.
Most programs are synchronous. They execute line by line in the order they’re written. Whenever we encounter an operation that can’t be completed immediately, we must wait until it finishes before we can proceed. Establishing a TCP connection, for example, requires several exchanges over a network, which can take some time, during which our program is unable to work on anything else. In other words, synchronous operations block the thread on which our program executes.
Asynchronous operations, on the other hand, are non-blocking by design. Operations that can’t complete immediately are suspended to the “background” (more on this later), allowing the current thread to work on other tasks. Once an async operation finishes, the task is unsuspended and execution resumes where it left off. By working jointly on multiple tasks a program can avoid being idle. When one task can’t progress because it’s waiting on another resource (e.g. bytes from a socket), we simply switch to another task that can. This brings us to our first observation about asynchronous programming in general: we only benefit if there is other work to be done while we wait on an operation to finish.