113 lines
6.7 KiB
HTML
113 lines
6.7 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Futures, Tasks, and Threads</title>
|
||
</head>
|
||
<body>
|
||
<h2 id="putting-it-all-together-futures-tasks-and-threads"><a class="header" href="#putting-it-all-together-futures-tasks-and-threads">Putting It All Together: Futures, Tasks, and Threads</a></h2>
|
||
<p>As we saw in <a href="http://localhost:3000/ch16-00-concurrency.html">Chapter 16</a><!-- ignore -->, threads provide one approach to
|
||
concurrency. We’ve seen another approach in this chapter: using async with
|
||
futures and streams. If you’re wondering when to choose one method over the other,
|
||
the answer is: it depends! And in many cases, the choice isn’t threads <em>or</em>
|
||
async but rather threads <em>and</em> async.</p>
|
||
<p>Many operating systems have supplied threading-based concurrency models for
|
||
decades now, and many programming languages support them as a result. However,
|
||
these models are not without their tradeoffs. On many operating systems, they
|
||
use a fair bit of memory for each thread. Threads are also only an option when
|
||
your operating system and hardware support them. Unlike mainstream desktop and
|
||
mobile computers, some embedded systems don’t have an OS at all, so they also
|
||
don’t have threads.</p>
|
||
<p>The async model provides a different—and ultimately complementary—set of
|
||
tradeoffs. In the async model, concurrent operations don’t require their own
|
||
threads. Instead, they can run on tasks, as when we used <code>trpl::spawn_task</code> to
|
||
kick off work from a synchronous function in the streams section. A task is
|
||
similar to a thread, but instead of being managed by the operating system, it’s
|
||
managed by library-level code: the runtime.</p>
|
||
<p>There’s a reason the APIs for spawning threads and spawning tasks are so
|
||
similar. Threads act as a boundary for sets of synchronous operations;
|
||
concurrency is possible <em>between</em> threads. Tasks act as a boundary for sets of
|
||
<em>asynchronous</em> operations; concurrency is possible both <em>between</em> and <em>within</em>
|
||
tasks, because a task can switch between futures in its body. Finally, futures
|
||
are Rust’s most granular unit of concurrency, and each future may represent a
|
||
tree of other futures. The runtime—specifically, its executor—manages tasks,
|
||
and tasks manage futures. In that regard, tasks are similar to lightweight,
|
||
runtime-managed threads with added capabilities that come from being managed by
|
||
a runtime instead of by the operating system.</p>
|
||
<p>This doesn’t mean that async tasks are always better than threads (or vice
|
||
versa). Concurrency with threads is in some ways a simpler programming model
|
||
than concurrency with <code>async</code>. That can be a strength or a weakness. Threads are
|
||
somewhat “fire and forget”; they have no native equivalent to a future, so they
|
||
simply run to completion without being interrupted except by the operating
|
||
system itself.</p>
|
||
<p>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. In fact, under the hood, the runtime we’ve been
|
||
using—including the <code>spawn_blocking</code> and <code>spawn_task</code> functions—is multithreaded
|
||
by default! Many runtimes use an approach called <em>work stealing</em> to
|
||
transparently move tasks around between threads, based on how the threads are
|
||
currently being utilized, to improve the system’s overall performance. That
|
||
approach actually requires threads <em>and</em> tasks, and therefore futures.</p>
|
||
<p>When thinking about which method to use when, consider these rules of thumb:</p>
|
||
<ul>
|
||
<li>If the work is <em>very parallelizable</em> (that is, CPU-bound), such as processing
|
||
a bunch of data where each part can be processed separately, threads are a
|
||
better choice.</li>
|
||
<li>If the work is <em>very concurrent</em> (that is, I/O-bound), such as handling
|
||
messages from a bunch of different sources that may come in at different
|
||
intervals or different rates, async is a better choice.</li>
|
||
</ul>
|
||
<p>And if you need both parallelism and concurrency, you don’t have to choose
|
||
between threads and async. You can use them together freely, letting each
|
||
play the part it’s best at. For example, Listing 17-25 shows a fairly common
|
||
example of this kind of mix in real-world Rust code.</p>
|
||
<figure class="listing" id="listing-17-25">
|
||
<span class="file-name">Filename: src/main.rs</span>
|
||
<pre class="playground"><code class="language-rust edition2024"><span class="boring">extern crate trpl; // for mdbook test
|
||
</span><span class="boring">
|
||
</span>use std::{thread, time::Duration};
|
||
|
||
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::block_on(async {
|
||
while let Some(message) = rx.recv().await {
|
||
println!("{message}");
|
||
}
|
||
});
|
||
}</code></pre>
|
||
<figcaption><a href="#listing-17-25">Listing 17-25</a>: Sending messages with blocking code in a thread and awaiting the messages in an async block</figcaption>
|
||
</figure>
|
||
<p>We begin by creating an async channel, then spawning a thread that takes
|
||
ownership of the sender side of the channel using the <code>move</code> keyword. Within
|
||
the thread, we send the numbers 1 through 10, sleeping for a second between
|
||
each. Finally, we run a future created with an async block passed to
|
||
<code>trpl::block_on</code> just as we have throughout the chapter. In that future, we
|
||
await those messages, just as in the other message-passing examples we have
|
||
seen.</p>
|
||
<p>To return to the scenario we opened the chapter with, imagine running a set of
|
||
video encoding tasks using a dedicated thread (because video encoding is
|
||
compute-bound) but notifying the UI that those operations are done with an
|
||
async channel. There are countless examples of these kinds of combinations in
|
||
real-world use cases.</p>
|
||
<h2 id="summary"><a class="header" href="#summary">Summary</a></h2>
|
||
<p>This isn’t the last you’ll see of concurrency in this book. The project in
|
||
<a href="../ch21/ch21-00-final-project-a-web-server.html">Chapter 21</a><!-- ignore --> will apply these concepts in a more realistic
|
||
situation than the simpler examples discussed here and compare problem-solving
|
||
with threading versus tasks and futures more directly.</p>
|
||
<p>No matter which of these approaches you choose, Rust gives you the tools you
|
||
need to write safe, fast, concurrent code—whether for a high-throughput web
|
||
server or an embedded operating system.</p>
|
||
<p>Next, we’ll talk about idiomatic ways to model problems and structure solutions
|
||
as your Rust programs get bigger. In addition, we’ll discuss how Rust’s idioms
|
||
relate to those you might be familiar with from object-oriented programming.</p>
|
||
</body>
|
||
</html>
|