394 lines
20 KiB
HTML
394 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Working With Any Number of Futures</title>
|
||
</head>
|
||
<body>
|
||
<!-- Old headings. Do not remove or links may break. -->
|
||
<p><a id="yielding"></a></p>
|
||
<h3 id="yielding-control-to-the-runtime"><a class="header" href="#yielding-control-to-the-runtime">Yielding Control to the Runtime</a></h3>
|
||
<p>Recall from the <a href="ch17-01-futures-and-syntax.html#our-first-async-program">“Our First Async Program”</a><!-- ignore -->
|
||
section 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 isn’t ready. The
|
||
inverse is also true: Rust <em>only</em> pauses async blocks and hands control back to
|
||
a runtime at an await point. Everything between await points is synchronous.</p>
|
||
<p>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 <em>starving</em> other futures. In some 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 that will keep doing some
|
||
particular task indefinitely, you’ll need to think about when and where to hand
|
||
control back to the runtime.</p>
|
||
<p>Let’s simulate a long-running operation to illustrate the starvation problem,
|
||
then explore how to solve it. Listing 17-14 introduces a <code>slow</code> function.</p>
|
||
<figure class="listing" id="listing-17-14">
|
||
<span class="file-name">Filename: src/main.rs</span>
|
||
<pre class="playground"><code class="language-rust edition2024"><span class="boring">extern crate trpl; // required for mdbook test
|
||
</span><span class="boring">
|
||
</span><span class="boring">use std::{thread, time::Duration};
|
||
</span><span class="boring">
|
||
</span><span class="boring">fn main() {
|
||
</span><span class="boring"> trpl::block_on(async {
|
||
</span><span class="boring"> // We will call `slow` here later
|
||
</span><span class="boring"> });
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span>fn slow(name: &str, ms: u64) {
|
||
thread::sleep(Duration::from_millis(ms));
|
||
println!("'{name}' ran for {ms}ms");
|
||
}</code></pre>
|
||
<figcaption><a href="#listing-17-14">Listing 17-14</a>: Using <code>thread::sleep</code> to simulate slow operations</figcaption>
|
||
</figure>
|
||
<p>This code uses <code>std::thread::sleep</code> instead of <code>trpl::sleep</code> so that calling
|
||
<code>slow</code> will block the current thread for some number of milliseconds. We can
|
||
use <code>slow</code> to stand in for real-world operations that are both long-running and
|
||
blocking.</p>
|
||
<p>In Listing 17-15, we use <code>slow</code> to emulate doing this kind of CPU-bound work in
|
||
a pair of futures.</p>
|
||
<figure class="listing" id="listing-17-15">
|
||
<span class="file-name">Filename: src/main.rs</span>
|
||
<pre class="playground"><code class="language-rust edition2024"><span class="boring">extern crate trpl; // required for mdbook test
|
||
</span><span class="boring">
|
||
</span><span class="boring">use std::{thread, time::Duration};
|
||
</span><span class="boring">
|
||
</span><span class="boring">fn main() {
|
||
</span><span class="boring"> trpl::block_on(async {
|
||
</span> let a = async {
|
||
println!("'a' started.");
|
||
slow("a", 30);
|
||
slow("a", 10);
|
||
slow("a", 20);
|
||
trpl::sleep(Duration::from_millis(50)).await;
|
||
println!("'a' finished.");
|
||
};
|
||
|
||
let b = async {
|
||
println!("'b' started.");
|
||
slow("b", 75);
|
||
slow("b", 10);
|
||
slow("b", 15);
|
||
slow("b", 350);
|
||
trpl::sleep(Duration::from_millis(50)).await;
|
||
println!("'b' finished.");
|
||
};
|
||
|
||
trpl::select(a, b).await;
|
||
<span class="boring"> });
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span><span class="boring">fn slow(name: &str, ms: u64) {
|
||
</span><span class="boring"> thread::sleep(Duration::from_millis(ms));
|
||
</span><span class="boring"> println!("'{name}' ran for {ms}ms");
|
||
</span><span class="boring">}</span></code></pre>
|
||
<figcaption><a href="#listing-17-15">Listing 17-15</a>: Calling the <code>slow</code> function to simulate slow operations</figcaption>
|
||
</figure>
|
||
<p>Each future hands control back to the runtime only <em>after</em> carrying out a bunch
|
||
of slow operations. If you run this code, you will see this output:</p>
|
||
<!-- manual-regeneration
|
||
cd listings/ch17-async-await/listing-17-15/
|
||
cargo run
|
||
copy just the output
|
||
-->
|
||
<pre><code class="language-text">'a' started.
|
||
'a' ran for 30ms
|
||
'a' ran for 10ms
|
||
'a' ran for 20ms
|
||
'b' started.
|
||
'b' ran for 75ms
|
||
'b' ran for 10ms
|
||
'b' ran for 15ms
|
||
'b' ran for 350ms
|
||
'a' finished.
|
||
</code></pre>
|
||
<p>As with Listing 17-5 where we used <code>trpl::select</code> to race futures fetching two
|
||
URLs, <code>select</code> still finishes as soon as <code>a</code> is done. There’s no interleaving
|
||
between the calls to <code>slow</code> in the two futures, though. The <code>a</code> future does all
|
||
of its work until the <code>trpl::sleep</code> call is awaited, then the <code>b</code> future does
|
||
all of its work until its own <code>trpl::sleep</code> call is awaited, and finally the
|
||
<code>a</code> future completes. To allow both futures to make progress between their slow
|
||
tasks, we need await points so we can hand control back to the runtime. That
|
||
means we need something we can await!</p>
|
||
<p>We can already see this kind of handoff happening in Listing 17-15: if we
|
||
removed the <code>trpl::sleep</code> at the end of the <code>a</code> future, it would complete
|
||
without the <code>b</code> future running <em>at all</em>. Let’s try using the <code>trpl::sleep</code>
|
||
function as a starting point for letting operations switch off making progress,
|
||
as shown in Listing 17-16.</p>
|
||
<figure class="listing" id="listing-17-16">
|
||
<span class="file-name">Filename: src/main.rs</span>
|
||
<pre class="playground"><code class="language-rust edition2024"><span class="boring">extern crate trpl; // required for mdbook test
|
||
</span><span class="boring">
|
||
</span><span class="boring">use std::{thread, time::Duration};
|
||
</span><span class="boring">
|
||
</span><span class="boring">fn main() {
|
||
</span><span class="boring"> trpl::block_on(async {
|
||
</span> let one_ms = Duration::from_millis(1);
|
||
|
||
let a = async {
|
||
println!("'a' started.");
|
||
slow("a", 30);
|
||
trpl::sleep(one_ms).await;
|
||
slow("a", 10);
|
||
trpl::sleep(one_ms).await;
|
||
slow("a", 20);
|
||
trpl::sleep(one_ms).await;
|
||
println!("'a' finished.");
|
||
};
|
||
|
||
let b = async {
|
||
println!("'b' started.");
|
||
slow("b", 75);
|
||
trpl::sleep(one_ms).await;
|
||
slow("b", 10);
|
||
trpl::sleep(one_ms).await;
|
||
slow("b", 15);
|
||
trpl::sleep(one_ms).await;
|
||
slow("b", 350);
|
||
trpl::sleep(one_ms).await;
|
||
println!("'b' finished.");
|
||
};
|
||
<span class="boring">
|
||
</span><span class="boring"> trpl::select(a, b).await;
|
||
</span><span class="boring"> });
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span><span class="boring">fn slow(name: &str, ms: u64) {
|
||
</span><span class="boring"> thread::sleep(Duration::from_millis(ms));
|
||
</span><span class="boring"> println!("'{name}' ran for {ms}ms");
|
||
</span><span class="boring">}</span></code></pre>
|
||
<figcaption><a href="#listing-17-16">Listing 17-16</a>: Using <code>trpl::sleep</code> to let operations switch off making progress</figcaption>
|
||
</figure>
|
||
<p>We’ve added <code>trpl::sleep</code> calls with await points between each call to <code>slow</code>.
|
||
Now the two futures’ work is interleaved:</p>
|
||
<!-- manual-regeneration
|
||
cd listings/ch17-async-await/listing-17-16
|
||
cargo run
|
||
copy just the output
|
||
-->
|
||
<pre><code class="language-text">'a' started.
|
||
'a' ran for 30ms
|
||
'b' started.
|
||
'b' ran for 75ms
|
||
'a' ran for 10ms
|
||
'b' ran for 10ms
|
||
'a' ran for 20ms
|
||
'b' ran for 15ms
|
||
'a' finished.
|
||
</code></pre>
|
||
<p>The <code>a</code> future still runs for a bit before handing off control to <code>b</code>, because
|
||
it calls <code>slow</code> before ever calling <code>trpl::sleep</code>, but after that the futures
|
||
swap back and forth each time one of them hits an await point. In this case, we
|
||
have done that after every call to <code>slow</code>, but we could break up the work in
|
||
whatever way makes the most sense to us.</p>
|
||
<p>We don’t really want to <em>sleep</em> here, though: we want to make progress as fast
|
||
as we can. We just need to hand back control to the runtime. We can do that
|
||
directly, using the <code>trpl::yield_now</code> function. In Listing 17-17, we replace
|
||
all those <code>trpl::sleep</code> calls with <code>trpl::yield_now</code>.</p>
|
||
<figure class="listing" id="listing-17-17">
|
||
<span class="file-name">Filename: src/main.rs</span>
|
||
<pre class="playground"><code class="language-rust edition2024"><span class="boring">extern crate trpl; // required for mdbook test
|
||
</span><span class="boring">
|
||
</span><span class="boring">use std::{thread, time::Duration};
|
||
</span><span class="boring">
|
||
</span><span class="boring">fn main() {
|
||
</span><span class="boring"> trpl::block_on(async {
|
||
</span> let a = async {
|
||
println!("'a' started.");
|
||
slow("a", 30);
|
||
trpl::yield_now().await;
|
||
slow("a", 10);
|
||
trpl::yield_now().await;
|
||
slow("a", 20);
|
||
trpl::yield_now().await;
|
||
println!("'a' finished.");
|
||
};
|
||
|
||
let b = async {
|
||
println!("'b' started.");
|
||
slow("b", 75);
|
||
trpl::yield_now().await;
|
||
slow("b", 10);
|
||
trpl::yield_now().await;
|
||
slow("b", 15);
|
||
trpl::yield_now().await;
|
||
slow("b", 350);
|
||
trpl::yield_now().await;
|
||
println!("'b' finished.");
|
||
};
|
||
<span class="boring">
|
||
</span><span class="boring"> trpl::select(a, b).await;
|
||
</span><span class="boring"> });
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span><span class="boring">fn slow(name: &str, ms: u64) {
|
||
</span><span class="boring"> thread::sleep(Duration::from_millis(ms));
|
||
</span><span class="boring"> println!("'{name}' ran for {ms}ms");
|
||
</span><span class="boring">}</span></code></pre>
|
||
<figcaption><a href="#listing-17-17">Listing 17-17</a>: Using <code>yield_now</code> to let operations switch off making progress</figcaption>
|
||
</figure>
|
||
<p>This code is both clearer about the actual intent and can be significantly
|
||
faster than using <code>sleep</code>, because timers such as the one used by <code>sleep</code> often
|
||
have limits on how granular they can be. The version of <code>sleep</code> we are using,
|
||
for example, will always sleep for at least a millisecond, even if we pass it a
|
||
<code>Duration</code> of one nanosecond. Again, modern computers are <em>fast</em>: they can do a
|
||
lot in one millisecond!</p>
|
||
<p>This means that async can be useful even for compute-bound tasks, depending on
|
||
what else your program is doing, because it provides a useful tool for
|
||
structuring the relationships between different parts of the program (but at a
|
||
cost of the overhead of the async state machine). This is a form of
|
||
<em>cooperative multitasking</em>, where each future has the power to determine when
|
||
it hands over control via await points. Each future therefore also has the
|
||
responsibility to avoid blocking for too long. In some Rust-based embedded
|
||
operating systems, this is the <em>only</em> kind of multitasking!</p>
|
||
<p>In real-world code, you won’t usually be alternating function calls with await
|
||
points on every single line, of course. While yielding control in this way is
|
||
relatively inexpensive, it’s not free. In many cases, trying to break up a
|
||
compute-bound task might make it significantly slower, so sometimes it’s better
|
||
for <em>overall</em> performance to let an operation block briefly. Always
|
||
measure to see what your code’s actual performance bottlenecks are. The
|
||
underlying dynamic is important to keep in mind, though, if you <em>are</em> seeing a
|
||
lot of work happening in serial that you expected to happen concurrently!</p>
|
||
<h3 id="building-our-own-async-abstractions"><a class="header" href="#building-our-own-async-abstractions">Building Our Own Async Abstractions</a></h3>
|
||
<p>We can also compose futures together to create new patterns. For example, we can
|
||
build a <code>timeout</code> function with async building blocks we already have. When
|
||
we’re done, the result will be another building block we could use to create
|
||
still more async abstractions.</p>
|
||
<p>Listing 17-18 shows how we would expect this <code>timeout</code> to work with a slow
|
||
future.</p>
|
||
<figure class="listing" id="listing-17-18">
|
||
<span class="file-name">Filename: src/main.rs</span>
|
||
<pre><code class="language-rust ignore does_not_compile"><span class="boring">extern crate trpl; // required for mdbook test
|
||
</span><span class="boring">
|
||
</span><span class="boring">use std::time::Duration;
|
||
</span><span class="boring">
|
||
</span><span class="boring">fn main() {
|
||
</span><span class="boring"> trpl::block_on(async {
|
||
</span> let slow = async {
|
||
trpl::sleep(Duration::from_secs(5)).await;
|
||
"Finally finished"
|
||
};
|
||
|
||
match timeout(slow, Duration::from_secs(2)).await {
|
||
Ok(message) => println!("Succeeded with '{message}'"),
|
||
Err(duration) => {
|
||
println!("Failed after {} seconds", duration.as_secs())
|
||
}
|
||
}
|
||
<span class="boring"> });
|
||
</span><span class="boring">}</span></code></pre>
|
||
<figcaption><a href="#listing-17-18">Listing 17-18</a>: Using our imagined <code>timeout</code> to run a slow operation with a time limit</figcaption>
|
||
</figure>
|
||
<p>Let’s implement this! To begin, let’s think about the API for <code>timeout</code>:</p>
|
||
<ul>
|
||
<li>It needs to be an async function itself so we can await it.</li>
|
||
<li>Its first parameter should be a future to run. We can make it generic to allow
|
||
it to work with any future.</li>
|
||
<li>Its second parameter will be the maximum time to wait. If we use a <code>Duration</code>,
|
||
that will make it easy to pass along to <code>trpl::sleep</code>.</li>
|
||
<li>It should return a <code>Result</code>. If the future completes successfully, the
|
||
<code>Result</code> will be <code>Ok</code> with the value produced by the future. If the timeout
|
||
elapses first, the <code>Result</code> will be <code>Err</code> with the duration that the timeout
|
||
waited for.</li>
|
||
</ul>
|
||
<p>Listing 17-19 shows this declaration.</p>
|
||
<!-- This is not tested because it intentionally does not compile. -->
|
||
<figure class="listing" id="listing-17-19">
|
||
<span class="file-name">Filename: src/main.rs</span>
|
||
<pre><code class="language-rust ignore does_not_compile"><span class="boring">extern crate trpl; // required for mdbook test
|
||
</span><span class="boring">
|
||
</span><span class="boring">use std::time::Duration;
|
||
</span><span class="boring">
|
||
</span><span class="boring">fn main() {
|
||
</span><span class="boring"> trpl::block_on(async {
|
||
</span><span class="boring"> let slow = async {
|
||
</span><span class="boring"> trpl::sleep(Duration::from_secs(5)).await;
|
||
</span><span class="boring"> "Finally finished"
|
||
</span><span class="boring"> };
|
||
</span><span class="boring">
|
||
</span><span class="boring"> match timeout(slow, Duration::from_secs(2)).await {
|
||
</span><span class="boring"> Ok(message) => println!("Succeeded with '{message}'"),
|
||
</span><span class="boring"> Err(duration) => {
|
||
</span><span class="boring"> println!("Failed after {} seconds", duration.as_secs())
|
||
</span><span class="boring"> }
|
||
</span><span class="boring"> }
|
||
</span><span class="boring"> });
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span>async fn timeout<F: Future>(
|
||
future_to_try: F,
|
||
max_time: Duration,
|
||
) -> Result<F::Output, Duration> {
|
||
// Here is where our implementation will go!
|
||
}</code></pre>
|
||
<figcaption><a href="#listing-17-19">Listing 17-19</a>: Defining the signature of <code>timeout</code></figcaption>
|
||
</figure>
|
||
<p>That satisfies our goals for the types. Now let’s think about the <em>behavior</em> we
|
||
need: we want to race the future passed in against the duration. We can use
|
||
<code>trpl::sleep</code> to make a timer future from the duration, and use <code>trpl::select</code>
|
||
to run that timer with the future the caller passes in.</p>
|
||
<p>In Listing 17-20, we implement <code>timeout</code> by matching on the result of awaiting
|
||
<code>trpl::select</code>.</p>
|
||
<figure class="listing" id="listing-17-20">
|
||
<span class="file-name">Filename: src/main.rs</span>
|
||
<pre class="playground"><code class="language-rust edition2024"><span class="boring">extern crate trpl; // required for mdbook test
|
||
</span><span class="boring">
|
||
</span><span class="boring">use std::time::Duration;
|
||
</span><span class="boring">
|
||
</span>use trpl::Either;
|
||
|
||
// --snip--
|
||
|
||
<span class="boring">fn main() {
|
||
</span><span class="boring"> trpl::block_on(async {
|
||
</span><span class="boring"> let slow = async {
|
||
</span><span class="boring"> trpl::sleep(Duration::from_secs(5)).await;
|
||
</span><span class="boring"> "Finally finished"
|
||
</span><span class="boring"> };
|
||
</span><span class="boring">
|
||
</span><span class="boring"> match timeout(slow, Duration::from_secs(2)).await {
|
||
</span><span class="boring"> Ok(message) => println!("Succeeded with '{message}'"),
|
||
</span><span class="boring"> Err(duration) => {
|
||
</span><span class="boring"> println!("Failed after {} seconds", duration.as_secs())
|
||
</span><span class="boring"> }
|
||
</span><span class="boring"> }
|
||
</span><span class="boring"> });
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span>async fn timeout<F: Future>(
|
||
future_to_try: F,
|
||
max_time: Duration,
|
||
) -> Result<F::Output, Duration> {
|
||
match trpl::select(future_to_try, trpl::sleep(max_time)).await {
|
||
Either::Left(output) => Ok(output),
|
||
Either::Right(_) => Err(max_time),
|
||
}
|
||
}</code></pre>
|
||
<figcaption><a href="#listing-17-20">Listing 17-20</a>: Defining <code>timeout</code> with <code>select</code> and <code>sleep</code></figcaption>
|
||
</figure>
|
||
<p>The implementation of <code>trpl::select</code> is not fair: it always polls arguments in
|
||
the order in which they are passed (other <code>select</code> implementations will
|
||
randomly choose which argument to poll first). Thus, we pass <code>future_to_try</code> to
|
||
<code>select</code> first so it gets a chance to complete even if <code>max_time</code> is a very
|
||
short duration. If <code>future_to_try</code> finishes first, <code>select</code> will return <code>Left</code>
|
||
with the output from <code>future_to_try</code>. If <code>timer</code> finishes first, <code>select</code> will
|
||
return <code>Right</code> with the timer’s output of <code>()</code>.</p>
|
||
<p>If the <code>future_to_try</code> succeeds and we get a <code>Left(output)</code>, we return
|
||
<code>Ok(output)</code>. If the sleep timer elapses instead and we get a <code>Right(())</code>, we
|
||
ignore the <code>()</code> with <code>_</code> and return <code>Err(max_time)</code> instead.</p>
|
||
<p>With that, we have a working <code>timeout</code> built out of two other async helpers. If
|
||
we run our code, it will print the failure mode after the timeout:</p>
|
||
<pre><code class="language-text">Failed after 2 seconds
|
||
</code></pre>
|
||
<p>Because futures compose with other futures, you can build really powerful tools
|
||
using smaller async building blocks. For example, you can use this same
|
||
approach to combine timeouts with retries, and in turn use those with
|
||
operations such as network calls (such as those in Listing 17-5).</p>
|
||
<p>In practice, you’ll usually work directly with <code>async</code> and <code>await</code>, and
|
||
secondarily with functions such as <code>select</code> and macros such as the <code>join!</code>
|
||
macro to control how the outermost futures are executed.</p>
|
||
<p>We’ve now seen a number of ways to work with multiple futures at the same time.
|
||
Up next, we’ll look at how we can work with multiple futures in a sequence over
|
||
time with <em>streams</em>.</p>
|
||
</body>
|
||
</html>
|