RustBrock/Async, Await, Futures and Streams.md
darkicewolf50 8f01c0e0e4
Some checks failed
Test Gitea Actions / first (push) Successful in 16s
Test Gitea Actions / check-code (push) Failing after 15s
Test Gitea Actions / test (push) Has been skipped
Test Gitea Actions / documentation-check (push) Has been skipped
finshed ch17.1
2025-03-19 16:16:19 -06:00

7.4 KiB

Fundamentals of Asynchronous Programming: Async, Await, Futures, and Streams

Many of the operations we ask computers to do can take a while to finish.

While waiting for that task it would be nice to do something else while we are waiting for those long-running processes to complete.

Modern computers offer two techniques while working on more than one operation at a time:

  • Parallelism
  • Concurrency

Once we start writing programs that involve parallel or concurrent operations, we quickly encounter new challenges inherit to asynchronous programming.

This is where operations may not finish sequentially in the order they were started.

This section builds on the use of threads for parallelism and concurrency by introducing an alternative approach to asynchronous programming.

Rust's Futures, Streams, the async and await syntax that supports them and the tools for managing and coordinating between asynchronous operations.

We will consider this example.

Say you are exporting a video you created of a family celebration, an operation that could take anywhere from minutes to hours.

The video export will use as many resources as available on the GPU and CPU.

If you only had one CPU core and your operating system didn't pause that export until it completed (it executed synchronously), you wouldn't be able to do anything else on the computer while that task was running.

This would be incredibly frustrating experience.

Fortunately, your computer's operating system can, and does, interrupt the export often enough to let you get other work done simultaneously.

In another scenario, say you are downloading a video from somewhere else, which can also take a while but does not take up as much CPU time.

In this case the CPU has to wait for data to arrive from the network.

While you can start reading the data once it starts to arrive, it may take some time for all of it to show up.

Even once the data us all present, if the video is quite large, it could take at least a second or two to load it all.

The video export is an example of a CPU-bound or compute-bound operation.

It is limited by the computer's potential data processing speed within the CPU or GPU, and how much speed it can dedicate to the operation.

The video download is an example of a IO-bound operation because it is limited by the speed of the computer's input and output; it can only go as fast as the data can be sent across the network.

In both of these examples, the operating system's invisible interrupts provide a form of concurrency.

This concurrency happens only at the level of the entire program: the operating system interrupts one program to let other programs get work done.

In many cases, because we understand our programs at a much more granular level than the OS does, we can spot opportunities for concurrency that the operating system can't see.

Lets say we are building a tool to manage file downloads, we should be able to write our program so that starting one download won't lock up the UI, and users should be able to start multiple downloads at the same time.

Many operating system API's for interacting with the network are blocking; that is they block the program's progress until the data they are processing is completely ready.

Note: This is how most functions calls work.

However, the term blocking is usually reserved for function calls that interact with files, the network, or other computer resources.

Due to these cases where an individual program would benefit from the operation being non-blocking.

We could avoid blocking our main thread by spawning a dedicated thread to download each file.

The overhead of those threads would eventually become a problem.

It would be preferable if the call didn't block it in the first place, it would be better if we could write in the same style we use in blocking code.

This would be a similar case/code

let data = fetch_data_from(url).await;
println!("{data}");

This is example what Rust's async (short for asynchronous) abstraction gives us.

This chapter we will learn about:

  • Futures and Async Syntax Section Link Here
  • How to use Rust's async and await syntax Section Link Here
  • How to use the async model to solve some of the same challenges we looked at in Ch 16 Section Link Here
  • How multithreading and async provide complementary solutions, that you can combine in many cases Section Link Here Before jumping into how async works in practice, we need o take a short detour to discuss the differences between parallelism and concurrency.

Parallelism and Concurrency

So far we treated parallelism and concurrency as mostly interchangeable so far.

Now we need to distinguish between them more precisely, because the differences will now start to show up.

Consider the different ways a team could split up work on a software project.

You could assign a single member multiple tasks assign each member one task or use a mix of the two approaches.

When an individual works on several different tasks before any of them is complete, this is concurrency.

Or maybe you have two different projects checked out on your computer and when you get bored or stuck on one project, you switch to the other.

As one person, so you can't make progress on both tasks at the exact same time, but you can multi-task, making progress on one at a time by switching between them.

Here is a picture of this When the team instead splits up a group of tasks by having each member take one task and work on it alone, this is parallelism

Each person on the team can make progress at the exact same time. In both of these workflows, you might have to coordinate between different tasks.

Maybe it was thought that the task was totally independent from every other.

It actually requires another person on the team to finish their task first..

Some of this work can be done in parallel, but some of it actually was serial: it could only happen in a series, one task after another.

Here is a diagram of this Likewise, you may realize that one of your own tasks depends on another of your tasks.

Now your concurrent work has also become serial.

Parallelism and concurrency can intersect with each other as well.

If you learn that colleague is stuck until you finish one of your tasks, you will probably focus all your efforts to "unblock" you colleague.

You and your coworkers are no longer able to work in parallel, and you are also no longer able to work concurrently on the task assigned.

The same dynamics come into play with software and hardware.

On a CPU core, the CPU can perform only one operation at a time but it can still work concurrently.

Using tools such as threads, processes and async, the computer can pasue one set of calculations and switch to others before cycling back to that first set of calculations again.

One a machine with multiple CPU cores, it can also do work in parallel.

One core can be performing one task while another core performs a completely unrelated one, and those operations actually happen at the same time.

When working with async in Rust, we are always dealing with concurrency.

Depending on the hardware, OS and the async runtime we use (more on async runtimes shortly), that concurrency many also use parallelism under the hood.