mirror of
https://github.com/rust-lang/book.git
synced 2026-05-26 18:52:07 -04:00
Ch. 17: address the rest of James’ review comments 🎉
Co-authored-by: James Munns <james@onevariable.com>
This commit is contained in:
@@ -322,18 +322,19 @@ to poll first. Regardless of whether the implementation of race we are using is
|
||||
fair, though, *one* of the futures will run up to the first `.await` in its body
|
||||
before another task can start.
|
||||
|
||||
Recall from [“What Are Futures?”][futures] that at each await point, Rust pauses
|
||||
the async block and hands control back to a runtime. The inverse is also true:
|
||||
Rust *only* pauses async blocks and hands control back to a runtime at an await
|
||||
point. Everything between await points is synchronous.
|
||||
Recall from [“What Are Futures?”][futures] that at each await point, Rust gives
|
||||
a runtime a chance to pause the task and switch to another one if the future
|
||||
being awaited is not ready. The inverse is also true: Rust *only* pauses async
|
||||
blocks and hands control back to a runtime at an await point. Everything between
|
||||
await points is synchronous.
|
||||
|
||||
That means if you do a bunch of work in an async block without an await point,
|
||||
that future will block any other futures from making progress. (You may
|
||||
sometimes hear this referred to as one future *starving* other futures. And this
|
||||
applies to threads, too!) In many cases, that may not be a big deal. However, if
|
||||
you are doing some kind of expensive setup or long-running work, or if you have
|
||||
a future which will keep doing some particular task indefinitely, you will need
|
||||
to think about when and where to hand control back to the runtime.
|
||||
that future will block any other futures from making progress. You may sometimes
|
||||
hear this referred to as one future *starving* other futures. In many cases,
|
||||
that may not be a big deal. However, if you are doing some kind of expensive
|
||||
setup or long-running work, or if you have a future which will keep doing some
|
||||
particular task indefinitely, you will need to think about when and where to
|
||||
hand control back to the runtime.
|
||||
|
||||
But *how* would you hand control back to the runtime in those cases?
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ However, they are not without their tradeoffs. On many operating systems, they
|
||||
use a fair bit of memory for each thread, and they come with some overhead for
|
||||
starting up and shutting down. Threads are also only an option when your
|
||||
operating system and hardware support them! Unlike mainstream desktop and mobile
|
||||
operating systems, many embedded operating systems, like those used on some
|
||||
microcontrollers, do not have OS-level threads at all.
|
||||
computers, some embedded systems do not have an OS at all, so they also do not
|
||||
have threads!
|
||||
|
||||
The async model provides a different—and ultimately complementary—set of
|
||||
tradeoffs. In the async model, concurrent operations do not require their own
|
||||
@@ -55,21 +55,20 @@ possible both *between* and *within* tasks. In that regard, tasks are kind of
|
||||
like lightweight, runtime-managed threads with added capabilities that come from
|
||||
being managed by a runtime instead of by the operating system. Futures are an
|
||||
even more granular unit of concurrency, where each future may represent a tree
|
||||
of other futures. <!-- TODO: here, somehow? What *is* the right mental model?
|
||||
-->
|
||||
of other futures. That is, the runtime—specifically, its executor—manages tasks,
|
||||
and tasks manage futures.
|
||||
|
||||
However, this does not mean that async tasks are always better than threads, any
|
||||
more than that threads are always better than tasks.
|
||||
|
||||
On the one hand, concurrency with threads is in some ways a simpler programming
|
||||
model than concurrency with `async`. Threads are somewhat “fire and forget”, and
|
||||
they only allow interaction with the rest of the program via tools like channels
|
||||
or their final result via `join`. On the other hand, they have no native
|
||||
equivalent to a future, so they simply run to completion, without interruption
|
||||
except by the operating system itself. Threads also have no mechanisms for
|
||||
cancellation—a subject we have not covered in depth in this chapter, but which
|
||||
is implicit in the fact that whenever we ended a future, its state got cleaned
|
||||
up correctly.
|
||||
model than concurrency with `async`. Threads are somewhat “fire and forget,”
|
||||
they have no native equivalent to a future, so they simply run to completion,
|
||||
without interruption except by the operating system itself. That is, they have
|
||||
no *intra-task concurrency* like futures can. Threads in Rust also have no
|
||||
mechanisms for cancellation—a subject we have not covered in depth in this
|
||||
chapter, but which is implicit in the fact that whenever we ended a future, its
|
||||
state got cleaned up correctly.
|
||||
|
||||
These limitations make threads harder to compose than futures. It is much more
|
||||
difficult, for example, to build something like the `timeout` we built in
|
||||
@@ -83,10 +82,11 @@ and how to group them. And it turns out that threads and tasks often work very
|
||||
well together, because tasks can (at least in some runtimes) be moved around
|
||||
between threads. We have not mentioned it up until now, but under the hood the
|
||||
`Runtime` we have been using, including the `spawn_blocking` and `spawn_task`
|
||||
functions, are multithreaded by default! Many runtimes can transparently move
|
||||
tasks around between threads based on the current utilization of the threads, to
|
||||
hopefully improve the overall performance of the system. To build that actually
|
||||
requires threads *and* tasks, and therefore futures.
|
||||
functions, is multithreaded by default! Many runtimes use an approach called
|
||||
*work stealing* to transparently move tasks around between threads based on the
|
||||
current utilization of the threads, with the aim of improving the overall
|
||||
performance of the system. To build that actually requires threads *and* tasks,
|
||||
and therefore futures.
|
||||
|
||||
As a default way of thinking about which to use when:
|
||||
|
||||
@@ -98,7 +98,47 @@ As a default way of thinking about which to use when:
|
||||
|
||||
And if you need some mix of parallelism and concurrency, you do not have to
|
||||
choose between threads and async. You can use them together freely, letting each
|
||||
one serve the part it is best at.
|
||||
one serve the part it is best at. For example, Listing 17-TODO shows a fairly
|
||||
common example of this kind of mix in real-world Rust code.
|
||||
|
||||
<!-- TODO: extract into a listing file! -->
|
||||
|
||||
<Listing number="17-TODO" caption="Sending messages with blocking code in a thread and awaiting the messages in an async block" file-name="src/main.rs">
|
||||
|
||||
```rust
|
||||
use std::thread;
|
||||
|
||||
fn main() {
|
||||
let (tx, mut rx) = trpl::channel();
|
||||
|
||||
thread::spawn(move || {
|
||||
for i in 1..11 {
|
||||
tx.send(i).unwrap();
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
});
|
||||
|
||||
trpl::run(async {
|
||||
while let Some(message) = rx.recv().await {
|
||||
println!("{message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
</Listing>
|
||||
|
||||
We begin by creating an async channel. Then we spawn a thread which takes
|
||||
ownership of the sender side of the channel. Within the thread, we send the
|
||||
numbers 1 through 10, and sleep for a second in between each. Finally, we run a
|
||||
future created with an async block passed to `trpl::run` just like we have
|
||||
throughout the chapter. In that future, we await those messages, just like in
|
||||
the other message-passing examples we have seen.
|
||||
|
||||
To return to the examples we opened the chapter with: you could imagine running
|
||||
a set of video encoding tasks using a dedicated thread, since video encoding is
|
||||
compute bound, but notifying the UI that those operations are done with an async
|
||||
channel. Examples of this kind of mix abound!
|
||||
|
||||
## Summary
|
||||
|
||||
|
||||
Reference in New Issue
Block a user