trpl-zh-cn/ch17-05-traits-for-async.html
2025-04-25 09:49:31 +00:00

349 lines
26 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE HTML>
<html lang="en" class="light sidebar-visible" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>深入理解 async 相关的 traits - Rust 程序设计语言 简体中文版</title>
<!-- Custom HTML head -->
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="icon" href="favicon.svg">
<link rel="shortcut icon" href="favicon.png">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/general.css">
<link rel="stylesheet" href="css/chrome.css">
<link rel="stylesheet" href="css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" id="highlight-css" href="highlight.css">
<link rel="stylesheet" id="tomorrow-night-css" href="tomorrow-night.css">
<link rel="stylesheet" id="ayu-highlight-css" href="ayu-highlight.css">
<!-- Custom theme stylesheets -->
<link rel="stylesheet" href="ferris.css">
<link rel="stylesheet" href="theme/2018-edition.css">
<link rel="stylesheet" href="theme/semantic-notes.css">
<link rel="stylesheet" href="theme/listing.css">
<!-- Provide site root to javascript -->
<script>
var path_to_root = "";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Start loading toc.js asap -->
<script src="toc.js"></script>
</head>
<body>
<div id="body-container">
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
const html = document.documentElement;
html.classList.remove('light')
html.classList.add(theme);
html.classList.add("js");
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<!-- populated by js -->
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
<noscript>
<iframe class="sidebar-iframe-outer" src="toc.html"></iframe>
</noscript>
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Rust 程序设计语言 简体中文版</h1>
<div class="right-buttons">
<a href="print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://github.com/KaiserY/trpl-zh-cn/tree/main" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
<a href="https://github.com/KaiserY/trpl-zh-cn/edit/main/src/ch17-05-traits-for-async.md" title="Suggest an edit" aria-label="Suggest an edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h2 id="深入理解-async-相关的-traits"><a class="header" href="#深入理解-async-相关的-traits">深入理解 async 相关的 traits</a></h2>
<blockquote>
<p><a href="https://github.com/rust-lang/book/blob/main/src/ch17-05-traits-for-async.md">ch17-05-traits-for-async.md</a>
<br>
commit 56ec353290429e6547109e88afea4de027b0f1a9</p>
</blockquote>
<p>贯穿本章,我们通过多种方式使用了 <code>Future</code><code>Pin</code><code>Unpin</code><code>Stream</code><code>StreamExt</code> trait。但是直到目前为止我们避免过多地了解它们如何工作或者如何组合在一起的细节这对你日常的 Rust 开发而言通常是没问题的。不过有时你会遇到需要了解更多细节的场景。在本小节,我们会足够深入以便理解这些场景,并仍会将 <em>真正</em> 有深度的内容留给其它文档。</p>
<h3 id="future-trait"><a class="header" href="#future-trait"><code>Future</code> trait</a></h3>
<p>让我们以更深入地了解 <code>Future</code> trait 作为开始。这里是 Rust 中其如何定义的:</p>
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
fn poll(self: Pin&lt;&amp;mut Self&gt;, cx: &amp;mut Context&lt;'_&gt;) -&gt; Poll&lt;Self::Output&gt;;
}
<span class="boring">}</span></code></pre></pre>
<p>trait 定义中包含一些的新类型和我们之前没有见过新语法,所以让我们逐步详细地解析一下这个定义。</p>
<p>首先, <code>Future</code> 的关联类型 <code>Output</code> 表明 future 最终解析出的类型。这类似于 <code>Iterator</code> trait 的关联类型 <code>Item</code>。其次,<code>Future</code> 还有一个 <code>poll</code> 方法,其有一个特殊的 <code>self</code> 参数的 <code>Pin</code> 引用和一个 <code>Context</code> 类型的可变引用,并返回一个 <code>Poll&lt;Self::Output&gt;</code>。稍后我们再细说 <code>Pin</code><code>Context</code>。现在让我们专注于方法返回的 <code>Poll</code> 类型:</p>
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>enum Poll&lt;T&gt; {
Ready(T),
Pending,
}
<span class="boring">}</span></code></pre></pre>
<p><code>Poll</code> 类型类似于一个 <code>Option</code>。它有一个包含值的变体 <code>Ready(T)</code>,和一个没有值的变体 <code>Pending</code>。不过 <code>Poll</code> 所代表的意义与 <code>Option</code> 非常不同!<code>Pending</code> 变体表明 future 仍然还有工作要进行,所有调用者稍后需要再次检查。<code>Ready</code> 变体表明 future 已经完成了其工作并且 <code>T</code> 的值是可用的。</p>
<blockquote>
<p>注意:对于大部分功能,调用者不应在 future 返回 <code>Ready</code> 后再次调用 <code>poll</code>。很多 future 在完成后再次轮询会 panic。可以安全地再次轮询的 future 会在文档中显示地说明。这类似于 <code>Iterator::next</code> 的行为。</p>
</blockquote>
<p>当你见到使用 <code>await</code> 的代码时Rust 会在底层将其编译为调用 <code>poll</code> 的代码。如果你回头看下示例 17-4其在一个单个 URL 解析完成后打印出页面标题Rust 将其编译为一些类似(虽然不完全是)这样的代码:</p>
<pre><code class="language-rust ignore">match page_title(url).poll() {
Ready(page_title) =&gt; match page_title {
Some(title) =&gt; println!("The title for {url} was {title}"),
None =&gt; println!("{url} had no title"),
}
Pending =&gt; {
// But what goes here?
}
}</code></pre>
<p>如果 future 仍然是 <code>Pending</code> 的话我们应该做什么呢?我们需要某种方式不断重试,直到 future 最终准备好。换句话说,我们需要一个循环:</p>
<pre><code class="language-rust ignore">let mut page_title_fut = page_title(url);
loop {
match page_title_fut.poll() {
Ready(value) =&gt; match page_title {
Some(title) =&gt; println!("The title for {url} was {title}"),
None =&gt; println!("{url} had no title"),
}
Pending =&gt; {
// continue
}
}
}</code></pre>
<p>不过,如果 Rust 真的将代码精确地编译成那样,那么每一个 <code>await</code> 都会变成阻塞操作 -- 这恰恰与我们的目标相反相反Rust 确保循环可以将控制权交给一些可以暂停当前 future 转而去处理其它 future 并在之后再次检查当前 future 的内容。如你所见,这就是异步运行时,这种安排和协调的工作是其主要工作之一。</p>
<p>在本章前面的内容中,我们描述了等待 <code>rx.recv</code><code>recv</code> 调用返回一个 future并 await 轮询它的 future。我们注意到当信道关闭时运行时会暂停 future 直到它就绪并返回 <code>Some(message)</code><code>None</code> 为止。随着我们对 <code>Future</code> trait尤其是 <code>Future::poll</code> 的理解的深入,我们可以看出其是如何工作的。运行时知道 future 返回 <code>Poll::Pending</code> 时它还没有完成。反过来说,当 <code>poll</code> 返回 <code>Poll::Ready(Some(message))</code><code>Poll::Ready(None)</code> 时运行时知道 future <strong>已经</strong>完成了并继续运行。</p>
<p>运行时如何工作的具体细节超出了本书的范畴。不过关键在于理解 future 的基本机制:运行时<strong>轮询</strong>其所负责的每一个 future在它们还没有完成时使其休眠。</p>
<h3 id="pin-和-unpin-traits"><a class="header" href="#pin-和-unpin-traits"><code>Pin</code><code>Unpin</code> traits</a></h3>
<p>当我们在示例 17-16 中引入 pin 的概念时,我们遇到了一个很不友好的错误信息。这里再次展示其中相关的部分:</p>
<!-- manual-regeneration
cd listings/ch17-async-await/listing-17-16
cargo build
copy *only* the final `error` block from the errors
-->
<pre><code class="language-text">error[E0277]: `{async block@src/main.rs:10:23: 10:33}` cannot be unpinned
--&gt; src/main.rs:48:33
|
48 | trpl::join_all(futures).await;
| ^^^^^ the trait `Unpin` is not implemented for `{async block@src/main.rs:10:23: 10:33}`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current scope
= note: required for `Box&lt;{async block@src/main.rs:10:23: 10:33}&gt;` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--&gt; file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.30/src/future/join_all.rs:29:8
|
27 | pub struct JoinAll&lt;F&gt;
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`
</code></pre>
<p>这个错误信息不仅告诉我们需要对这些值进行 pin 操作,还解释了为什么 pin 是必要的。<code>trpl::join_all</code> 函数返回一个叫做 <code>JoinAll</code> 的结构体。这个结构体是一个 <code>F</code> 类型的泛型,它被限制为需要实现 <code>Future</code> trait。通过 <code>await</code> 直接 await 一个 future 会隐式地 pin 住这个函数。这也就是为什么我们不需要在任何想要 await future 的地方使用 <code>pin!</code></p>
<p>然而,这里我们没有直接 await 一个 future。相反我们通过向 <code>join_all</code> 函数传递一个 future 集合来构建了一个新 future <code>JoinAll</code><code>join_all</code> 的签名要求集合中项的类型都要实现 <code>Future</code> trait<code>Box&lt;T&gt;</code> 只有在其封装的 <code>T</code> 是一个实现了 <code>Unpin</code> trait 的 future 时才会实现 <code>Future</code></p>
<p>这有很多需要吸收的知识!为了真正地理解它,让我们稍微深入理解 <code>Future</code> 实际上是如何工作的,特别是 <em>pinning</em> 那一部分。</p>
<p>再次观察 <code>Future</code> trait 的定义:</p>
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
// Required method
fn poll(self: Pin&lt;&amp;mut Self&gt;, cx: &amp;mut Context&lt;'_&gt;) -&gt; Poll&lt;Self::Output&gt;;
}
<span class="boring">}</span></code></pre></pre>
<p>这里的 <code>cx</code> 参数及其 <code>Context</code> 类型,是运行时如何在仍保持 lazy 的情况下实际知道何时去检查任何给定的 future 的关键。同样,它们是如何工作的细节超出了本章的范畴,通常你只有在编写自定义 <code>Future</code> 实现时才需要思考它。相反我们将关注 <code>self</code> 的类型,因为这是第一次见到 <code>self</code> 有类型注解的方法。<code>self</code> 的类型注解与其它参数的类型注解类似,但有两个关键区别:</p>
<ul>
<li>它告诉 Rust 在调用该方法时 <code>self</code> 必须具备的类型。</li>
<li>它不能是任意类型。这限制了实现了该方法的类型,是一个该类型的引用或者智能指针,或者一个封装了该类型引用的 <code>Pin</code></li>
</ul>
<p>我们会在 <a href="ch18-00-oop.html">第十八章</a> 更多地看到这个语法。现在,知道如果要轮询一个 future 来检查它是 <code>Pending</code> 或者 <code>Ready(Output)</code>,我们需要一个 <code>Pin</code> 封装的该类型的可变引用就够了。</p>
<p><code>Pin</code> 是一个类指针类型的封装,比如 <code>&amp;</code><code>&amp;mut</code><code>Box</code><code>Rc</code>。(从技术上来说,<code>Pin</code> 适用于实现了 <code>Deref</code><code>DerefMut</code> trait 的类型,不过这实际上等同于只能适用于指针。)<code>Pin</code> 本身并不是一个指针并且也不具备类似 <code>Rc</code><code>Arc</code> 那样引用技术的功能;它单纯地是一个编译器可以用来约束指针使用的工具。</p>
<p>回忆一下 <code>await</code> 的实现是基于对 <code>poll</code> 的调用,这有助于解释之前见到的错误信息,不过那是关于 <code>Unpin</code> 的。所以 <code>Pin</code> 具体与 <code>Unpin</code> 有何关联,又为什么 <code>Future</code> 需要 <code>self</code> 在一个 <code>Pin</code> 类型中才能调用 <code>poll</code></p>
<p>回忆一下本章之前 future 中一系列的 await point 被编译为一个状态机,而编译器确保这个状态机遵守所有 Rust 在安全方面的正常规则包括借用和所有权。为了使其正常工作Rust 检查在一个 await point 和另一个 await point 之间或到异步代码块结尾之间什么数据是需要的。它接着在编译的状态机中创建一个相应的变体。每一个变体获得其在源码中对应片段所用到的数据的访问权限,要么获取数据的所有权要么获取其可变或不可变引用。</p>
<p>到目前为止, 一切正常:如果你在给定异步代码块中搞错了所有权或者引用,借用检查器会告诉你。当我们需要移动对应代码块的 future -- 例如将其移动到 <code>Vec</code> 中或者传递给 <code>join_all</code> -- 事情就会变得棘手。</p>
<p>当我们移动一个 future -- 将其移动进一个数据结构通过 <code>join_all</code> 将其用作迭代器或者将其从函数返回 -- 这实际上意味着将 Rust 创建的状态机移动给我们。同时不同于 Rust 中大部分其它类型Rust 为异步代码块创建的 future 可能最终会在任意给定变体的字段中有其自身的引用,如图 17-4 中的简化图所示。</p>
<figure>
<img alt="A single-column, three-row table representing a future, fut1, which has data values 0 and 1 in the first two rows and an arrow pointing from the third row back to the second row, representing an internal reference within the future." src="img/trpl17-04.svg" class="center" />
<figcaption>图 17-4: 一个自引用数据类型。</figcaption>
</figure>
<p>但是默认移动任何拥有其自身引用的对象是不安全unsafe因为引用总是会指向任何其引用数据的实际内存地址见图 17-5。如果我们移动数据结构本身这些内部引用会停留在指向老的地址。然后这些内存地址现在是无效的。一方面当修改这些数据结构时这些值不会被更新。另一个更重要的方面是电脑现在可以随意复用这些内存用于其它用途之后你最终可能会读取到完全不相关的数据。</p>
<figure>
<img alt="Two tables, depicting two futures, fut1 and fut2, each of which has one column and three rows, representing the result of having moved a future out of fut1 into fut2. The first, fut1, is grayed out, with a question mark in each index, representing unknown memory. The second, fut2, has 0 and 1 in the first and second rows and an arrow pointing from its third row back to the second row of fut1, representing a pointer that is referencing the old location in memory of the future before it was moved." src="img/trpl17-05.svg" class="center" />
<figcaption>图 17-5: 移动一个自引用数据类型的不安全结果。</figcaption>
</figure>
<p>理论上来说Rust 编译器也可以在对象被移动时尝试更新其所有的引用,不过这会增加很多性能开销,特别是在有一整个网状的引用需要更新的时候。相反如果我们确保相关的数据结构<strong>不能再内存中移动</strong>,我们就无需更新任何引用。这正是 Rust 的借用检查器所要求的:在安全代码中,禁止移动任何有自身活动引用的项。</p>
<p>构建于这之上的 <code>Pin</code> 正好给了我们所需的保证。当我们通过 <code>Pin</code> 封装一个值的引用来 <strong>pin</strong> 住它时,它就不能再移动了。也就是说,如果有 <code>Pin&lt;Box&lt;SomeType&gt;&gt;</code>,你实际上 ping 住了 <code>SomeType</code> 的值,而<strong>不是</strong> <code>Box</code> 指针。图 17-6 解释了这一过程。</p>
<figure>
<img alt="Three boxes laid out side by side. The first is labeled “Pin”, the second “b1”, and the third “pinned”. Within “pinned” is a table labeled “fut”, with a single column; it represents a future with cells for each part of the data structure. Its first cell has the value “0”, its second cell has an arrow coming out of it and pointing to the fourth and final cell, which has the value “1” in it, and the third cell has dashed lines and an ellipsis to indicate there may be other parts to the data structure. All together, the “fut” table represents a future which is self-referential. An arrow leaves the box labeled “Pin”, goes through the box labeled “b1” and has terminates inside the “pinned” box at the “fut” table." src="img/trpl17-06.svg" class="center" />
<figcaption>图 17-6: pin 住一个指向自引用 future 类型的 `Box`。</figcaption>
</figure>
<p>事实上,<code>Box</code> 指针仍然可以随意移动。请记住:我们关心确保最终被引用的数据保持不动。如果指针移动了,<strong>但是它指向的数据还在相同的位置</strong>,就像图 17-7 一样,就不会有潜在的问题。(作为一个独立的练习,查看一下 <code>Pin</code> 类型以及 <code>std::pin</code> 模块的文档来尝试理解如何通过 <code>Pin</code> 封装一个 <code>Box</code> 来做到这些。)其关键在于自引用类型本身不可移动,因为它仍然是被 pin 住的。</p>
<figure>
<img alt="Four boxes laid out in three rough columns, identical to the previous diagram with a change to the second column. Now there are two boxes in the second column, labeled “b1” and “b2”, “b1” is grayed out, and the arrow from “Pin” goes through “b2” instead of “b1”, indicating that the pointer has moved from “b1” to “b2”, but the data in “pinned” has not moved." src="img/trpl17-07.svg" class="center" />
<figcaption>图 17-7: 移动一个指向自引用 future 类型的 `Box`。</figcaption>
</figure>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="ch17-04-streams.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="ch17-06-futures-tasks-threads.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="ch17-04-streams.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="ch17-06-futures-tasks-threads.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script>
window.playground_copyable = true;
</script>
<script src="elasticlunr.min.js"></script>
<script src="mark.min.js"></script>
<script src="searcher.js"></script>
<script src="clipboard.min.js"></script>
<script src="highlight.js"></script>
<script src="book.js"></script>
<!-- Custom JS scripts -->
<script src="ferris.js"></script>
</div>
</body>
</html>