From 2014350df9f2323832ff3f036d7593e9fda7f993 Mon Sep 17 00:00:00 2001 From: fwqaaq Date: Mon, 17 Jul 2023 19:01:04 +0800 Subject: [PATCH] Review chapter 4 and 5 --- 4_Building_Our_Own_Spin_Lock.md | 14 +++++++------- 5_Building_Our_Own_Channels.md | 26 +++++++++++++------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/4_Building_Our_Own_Spin_Lock.md b/4_Building_Our_Own_Spin_Lock.md index ba0606d..3f68179 100644 --- a/4_Building_Our_Own_Spin_Lock.md +++ b/4_Building_Our_Own_Spin_Lock.md @@ -55,11 +55,11 @@ impl SpinLock { > false, true, Acquire, Relaxed).is_err() > ``` > -> 这更细节一点,但是根据你的思维,这可能会更容易理解,因为它更容易地表述了可能失败和可能成功的情况。然而,它也导致了稍微不同的指令,正如我们在[第七章](./7_Understanding_the_Processor.md)所看到的那样。 +> 这可能有点冗长,但是根据你的思维,这可能会更容易理解,因为它更容易地表述了可能失败和可能成功的情况。然而,它也导致了稍微不同的指令,正如我们将在[第七章](./7_Understanding_the_Processor.md)所看到的那样。 在 while 循环中,我们使用一个自旋循环提示,它告诉处理器我们正在自旋等待某些变化。在大多数平台上,该自旋导致处理器核心采取优化行为以应对这种情况。例如,它可能暂时地降低速度或优先处理其它有用的任务。然而,与 `thread::sleep` 或者 `thread::park` 等阻塞操作不同,自旋循环提示并不会调用操作系统调用,将你的线程置于睡眠状态以便执行其它线程。 -> 总的来说,在自旋循环中包含这样的提示是一个好的主意。根据情况,在尝试再次访问原子变量之前,最好多次执行此提示。如果你关心最后几纳秒的性能并且想要找到最优的策略,你将不得不为你特定用例编写基准测试。不幸地是,正如我们将在[第7章](./7_Understanding_the_Processor.md)中看到的那样,此类基准测试的结果可能在很大程度上取决于硬件。 +> 总的来说,在自旋循环中包含这样的提示是一个好的主意。根据情况,在尝试再次访问原子变量之前,最好多次执行此提示。如果你关心最后几纳秒的性能并且想要找到最优的策略,你将不得不为你特定用例编写基准测试。不幸的是,正如我们将在[第 7 章](./7_Understanding_the_Processor.md)中看到的那样,此类基准测试的结果可能在很大程度上取决于硬件。 我们可以使用 acquire 和 release 内存排序去确保每个 `unlock()` 调用和随后的 `lock()` 调用都建立了一个 happens-before 关系。换句话说,为了确保锁定它后,我们可以安全地假设上次锁定期间的任何事情已经发生。这是 acquire 和 release 最经典的使用案列:获取和释放一个锁。 @@ -87,13 +87,13 @@ pub struct SpinLock { } ``` -作为一种预防措施,UnsafeCell 没有实现 `Sync`,这意味着我们的类型现在不再可以在线程之间共享,使其变得毫无用处。为了修复它,我们需要向编译器保证我们的类型实际上可以在线程之间共享是安全的。然而,因为锁可以用于在线程之间发送类型为 T 的值,我们限制这个承诺为那些可以安全发送给其它线程的类型。因此,我们(不安全地)为所有实现 `Send` 的 T 实现 `SpinLock` 的 `Sync`,如下所示: +作为一种预防措施,UnsafeCell 没有实现 `Sync`,这意味着我们的类型现在不再可以在线程之间共享,使其变得毫无用处。为了修复它,我们需要向编译器保证我们的类型实际上可以在线程之间共享是安全的。然而,因为锁可以用于在线程之间发送类型为 T 的值,我们将这个承诺限制为哪些类型可以在线程之间安全发送。因此,我们(不安全地)为所有实现 `Send` 的 T 实现 `SpinLock` 的 `Sync`,如下所示: ```rust unsafe impl Sync for SpinLock where T: Send {} ``` -注意,我们并不需要去要求 T 是 `Sync`,由于我们的 `SpinLock` 仅一次允许一个线程访问它保护的 T。只有当我们同时允许多个线程访问权限时,就像读写锁对 reader 所做的那样,我们(另外)才需要 `T: Sync`。 +注意,我们并不需要去要求 T 是 `Sync`,由于我们的 `SpinLock` 一次仅允许一个线程访问它保护的 T。只有当我们同时允许多个线程访问时,就像读写锁对 reader 所做的那样,我们(另外)才需要 `T: Sync`。 下一步,现在我们的新函数需要接收一个 T 类型的值来初始化 `UnsafeCell`: @@ -110,7 +110,7 @@ impl SpinLock { } ``` -然后我们进入有趣的部分:锁定和解锁。我们做这一切的原因,是为了能够从 `lock()` 中返回 `&mut T`,例如,这样用户在使用我们的锁来保护它们的数据时,并不要求写不安全、未检查的代码。这意味着,我们现在在锁定的实现中不得不使用一个不安全的代码。`UnsafeCell` 可以通过其 `get()` 方法向我们提供指向其内容(`*mut T`)的原始指针,我们可以使用不安全块传唤到一个引用,如下所示: +然后我们进入有趣的部分:锁定和解锁。我们做这一切的原因,是为了能够从 `lock()` 中返回 `&mut T`,例如,这样用户在使用我们的锁来保护它们的数据时,并不要求写不安全、未检查的代码。这意味着,我们现在在锁定的实现中不得不使用一个不安全的代码。`UnsafeCell` 可以通过其 `get()` 方法向我们提供指向其内容(`*mut T`)的原始指针,我们可以使用不安全块转换到一个引用,如下所示: ```rust pub fn lock(&self) -> &mut T { @@ -127,7 +127,7 @@ impl SpinLock { pub fn lock<'a>(&'a self) -> &'a mut T { … } ``` -这清楚地表明,返回引用的生命周期与 `&self` 的生命周期相同。这意味着我们已经声称,只要锁本身存在,返回的引用就是有效的。 +这清楚的表明,返回引用的生命周期与 `&self` 的生命周期相同。这意味着我们已经声称,只要锁本身存在,返回的引用就是有效的。 如果我们假装 `unlock()` 不存在,这将是完全安全和健全的接口。SpinLock 可以被锁定,导致一个 `&mut T`,并且然后不再被再次锁定,这保证了这个独占引用确实是独占的。 @@ -143,7 +143,7 @@ pub fn lock<'a>(&self) -> &'a mut T { … } ``` -不幸地是,这并不是有效的 Rust。我们必须试图向用户解释这个限制,而不是向编译器解释。为了将责任转移到用户身上,我们将 `unlock` 函数标记为不安全,并给他们留下一张纸条,解释他们需要做什么来保持健全: +不幸的是,这并不是有效的 Rust。我们必须试图向用户解释这个限制,而不是向编译器解释。为了将责任转移到用户身上,我们将 `unlock` 函数标记为不安全,并给他们留下一张纸条,解释他们需要做什么来保持健全: ```rust /// 安全性:来自 lock() 的 &mut T 必须消失 diff --git a/5_Building_Our_Own_Channels.md b/5_Building_Our_Own_Channels.md index 3af764d..e64abe9 100644 --- a/5_Building_Our_Own_Channels.md +++ b/5_Building_Our_Own_Channels.md @@ -2,7 +2,7 @@ (英文版本) -*Channel* 可以被用于在线程之间发送数据,并且它们有很多变体。一些 channel 仅能在一个发送者和一个接收者之间使用,而另一些可以在任意数量的线程之间发送,或者甚至允许多个接收者。一些 channel 是阻塞的,这意味着接收(有时也包括发送)是一个阻塞操作,这会使线程进入睡眠状态,直到你的操作完成。一些 channel 针对吞吐量进行优化,而另一些针对低延迟进行优化。 +*Channel* 可以被用于在线程之间发送数据,并且它有很多变体。一些 channel 仅能在一个发送者和一个接收者之间使用,而另一些可以在任意数量的线程之间发送,或者甚至允许多个接收者。一些 channel 是阻塞的,这意味着接收(有时也包括发送)是一个阻塞操作,这会使线程进入睡眠状态,直到你的操作完成。一些 channel 针对吞吐量进行优化,而另一些针对低延迟进行优化。 这些变体是无穷尽的,没有一种通用版本在所有场景都适合的。 @@ -14,9 +14,9 @@ 一个基础的 channel 实现并不需要任何关于原子的知识。我们可以接收 `VecDeque`,它根本上是一个 `Vec`,允许在两端高效的添加和移除元素,并使用 Mutex 保护它,以允许多个线程访问。然后,我们使用 `VecDeque` 作为已发送但尚未接受数据的消息队列。任何想要发送消息的线程只需要将其添加到队列的末尾,而任何想要接受消息的线程只需从队列的前端删除一个消息。 -还有一件事需要补充,用于使接收操作阻塞:Condvar(参见[第一章“条件变量”](./1_Basic_of_Rust_Concurrency.md#条件变量)),以通知等待接收者新的消息。 +还有一点需要补充,用于将接收操作阻塞的 Condvar(参见[第一章“条件变量”](./1_Basic_of_Rust_Concurrency.md#条件变量)),当有新的消息,它会通知正在等待的接收者。 -这样做的实现可能非常简短且相对简单,如下所示: +这样做的实现可能非常简短且相对直接,如下所示: ```rust pub struct Channel { @@ -49,15 +49,15 @@ impl Channel { } ``` -注意,我们并没有使用任意的原子操作或者不安全代码,也不需要考虑 `Send` 或者 `Sync`。编译器理解 Mutex 的接口以及保证该提供什么类型,并且会隐式地理解如果 `Mutex` 和 Condvar 都可以在线程之间安全共享,那么我们的 `Channel` 也可以。 +注意,我们并没有使用任意的原子操作或者不安全代码,也不需要考虑 `Send` 或者 `Sync`。编译器理解 Mutex 的接口以及保证该提供什么类型,并且会隐式地理解,如果 `Mutex` 和 Condvar 都可以在线程之间安全共享,那么我们的 `Channel` 也可以这么做。 -我们的 `send` 函数锁定 mutex,将新消息推入队列的末尾,并且使用条件变量在解锁队列后直接通知可能等待的接收者。 +我们的 `send` 函数锁定 mutex,然后从队列的末尾推入消息,并且使用条件变量在解锁队列后直接通知可能等待的接收者。 `receive` 函数也锁定 mutex,然后从队列的首部弹出消息,但如果仍然没有可获得的消息,则会使用条件变量去等待。 > 记住,`Condvar::wait` 方法将在等待时解锁 Mutex,并在返回之前重新锁定它。因此,我们的 `receive` 函数将不会在等待时锁定 mutex。 -尽管这个 channel 在使用上是非常灵活的,因为它允许任意数量的发送和接收线程,它的实现在很多情况下远非最佳。即使有大量的消息准备好被接收,任意的发送或者接收操作将短暂地阻塞任意其它的发送或者接收操作,因为它们必须都锁定相同的 mutex。如果 `VecDeque::push` 不得不增加 VecDeque 的容量,所有的发送和接收线程将不得不等待该线程完成重新分配容量,这在某些情况下是不可接受的。 +尽管这个 channel 在使用上是非常灵活的,因为它允许任意数量的发送和接收线程,但在很多情况下,它的实现远非最佳。即使有大量的消息准备好被接收,任意的发送或者接收操作将短暂地阻塞任意其它的发送或者接收操作,因为它们必须都锁定相同的 mutex。如果 `VecDeque::push` 必须增加 VecDeque 的容量时,所有的发送和接收线程将不得不等待该线程完成重新分配容量,这在某些情况下是不可接受的。 另一个可能不可取的属性是,该 channel 的队列可能会无限制地增长。没有什么能阻止发送者以比接收者更高的速度持续发送新消息。 @@ -67,7 +67,7 @@ impl Channel { channel 的各种用例几乎是无止尽的。然而,在本章的剩余部分,我们将专注于一种特定类型的用例:恰好从一个线程向另一个线程发送一条消息。为此类用例设计的 channel 通常被称为 *一次性*(one-shot)channel。 -我们采用上述基于 `Mutex` 的实现,并且将 `VecDeque` 替换为 `Option`,从而将队列的容量减小到恰好一个消息。这样可以避免内存,但是仍然会有使用 Mutex 的一些缺点。我们可以通过使用原子操作从头构建我们自己的一次性 channel 来避免这个问题。 +我们采用上述基于 `Mutex` 的实现,并且将 `VecDeque` 替换为 `Option`,从而将队列的容量减小到恰好一个消息。这样可以避免内存,但仍然会存在使用 Mutex 的一些缺点。我们可以通过使用原子操作从头构建我们自己的一次性 channel 来避免这个问题。 首先,让我们构建一个最小化的一次性 channel 实现,不需要考虑它的接口。在本章的稍后,我们将探索如何改进其接口以及如何与 Rust 类型相结合,为 channel 的用于提供愉快的体验。 @@ -117,7 +117,7 @@ impl Channel { } ``` -在上面这个片段中,我们使用 `UnsafeCell::get` 方法去获取指向 `MaybeUninit` 的指针,并且通过不安全地解引用它来调用 `MaybeUninit::write` 进行初始化。当错误使用时,这可能导致未定义行为,但我们将这个责任注意到了调用方身上。 +在上面这个片段中,我们使用 `UnsafeCell::get` 方法去获取指向 `MaybeUninit` 的指针,并且通过不安全地解引用它来调用 `MaybeUninit::write` 进行初始化。当错误使用时,这可能导致未定义行为,但我们将这个责任转移到了调用方身上。 对于内存排序,我们需要使用 release 排序,因为原子的存储有效地将消息释放给接收者。这确保了如果接收线程从 `self.ready` 以 acquire 排序加载 true,则消息的初始化将从接受线程的角度完成。 @@ -143,7 +143,7 @@ impl Channel { 多次调用 send 可能会导致数据竞争,因为第二个发送者在接收者尝试读取第一条消息时可能正在覆盖数据。即使接收操作得到了正确的同步,从多个线程调用 send 可能会导致两个线程尝试并发地写入 cell,再次导致数据竞争。此外,多次调用 `receive` 会导致获取两个消息的副本,即使 T 不实现 `Copy` 并且因此不能安全地进行复制。 -更微妙的问题是我们的通道缺乏 `Drop` 实现。`MaybeUninit` 类型不会跟踪它是否已经初始化,因此它在被丢弃时不会自动丢弃其内容。这意味着如果发送了一条消息但从未被接收,该消息将永远不会被丢弃。这不是不正确的,但仍然是要避免。在 Rust 中,泄漏被普遍认为是安全的,但通常只有作为另一个泄漏的结果才是可接受的。例如,泄漏 Vec 也会泄漏其内容,但正常使用 Vec 不会导致任何泄漏。 +更微妙的问题是我们的 Channel 缺乏 `Drop` 实现。`MaybeUninit` 类型不会跟踪它是否已经初始化,因此它在被丢弃时不会自动丢弃其内容。这意味着如果发送了一条消息但从未被接收,该消息将永远不会被丢弃。这并不是不正确的,但仍然是要避免。在 Rust 中,泄漏被普遍认为是安全的,但通常只有作为另一个泄漏的后果才是可接受的。例如,泄漏 Vec 也会泄漏其内容,但正常使用 Vec 不会导致任何泄漏。 由于我们让用户对一切负责,不幸的事故只是时间问题。 @@ -181,7 +181,7 @@ impl Channel { > 记住,ready 上的总修改顺序(参见[第三章的“Relaxed 排序”](./3_Memory_Ordering.md#relaxed-排序))保证了从 `is_ready` 加载 true 之后,receive 也能看到 true。无论 is_ready 使用的内存排序如何,都不会出现 `is_ready` 返回 true,`receive()` 仍然出现 panic 的情况。 -下一个要解决的问题是,当调用 receive 不止一次时会发生什么。通过在接收者法中将 `ready` 标识设置回 false,我们也可以很容易地导致 panic,例如: +下一个要解决的问题是,当调用 receive 不止一次时会发生什么。通过在接收方法中将 `ready` 标识设置回 false,我们也可以很容易地导致 panic,例如: ```rust /// 如果仍然没有消息可获得, @@ -287,7 +287,7 @@ fn main() { thread '' panicked at 'can't send more than one message!', src/main.rs ``` -尽管 panic 程序并不出色,但是程序可靠的 panic 比坑的未定义行为错误好太多。 +尽管 panic 程序并不出色,但是程序可靠的 panic 比可能的未定义行为错误好太多。

为 Channel 状态使用单原子

@@ -513,7 +513,7 @@ note: this function takes ownership of the receiver `self`, which moves `sender` 不幸的是,可能会出现新的问题,这可能导致运行时开销。在这种情况下,问题是拆分所有权,我们不得不使用 Arc 并承受 Arc 的代价。 -不得不在安全性、便利性、灵活性、简单性和性能之间进行权衡是不幸的,但有时是不可避免的。Rust通常致力于在这些方面取得最佳表现,但有时为了最大化某个方面的优势,我们需要在其中做出一些妥协。 +不得不在安全性、便利性、灵活性、简单性和性能之间进行权衡是不幸的,但有时是不可避免的。Rust 通常致力于在这些方面取得最佳表现,但有时为了最大化某个方面的优势,我们需要在其中做出一些妥协。 ## 借用以避免内存分配 @@ -655,7 +655,7 @@ pub struct Sender<'a, T> { 然而,如果 Receiver 对象在线程之间发送,该句柄将引用错误的线程。Sender 将不会意识到这个,并且仍然会参考最初持有 Receiver 的线程。 -我们可以通过使 Receiver 更具限制性,不再允许它在线程之间发送来处理这个问题。正如[第1章“线程安全:Send 和 Sync”](./1_Basic_of_Rust_Concurrency.md#线程安全send-和-sync)中所讨论的,我们可以使用特殊的 `PhantomData` 标记类型将此限制添加到我们的结构中。`PhantomData<*const ()>` 将完成这项工作,因为原始指针,如 `*const ()`,不实现发送: +我们可以通过使 Receiver 更具限制性,不再允许它在线程之间发送来处理这个问题。正如[第 1 章“线程安全:Send 和 Sync”](./1_Basic_of_Rust_Concurrency.md#线程安全send-和-sync)中所讨论的,我们可以使用特殊的 `PhantomData` 标记类型将此限制添加到我们的结构中。`PhantomData<*const ()>` 将完成这项工作,因为原始指针,如 `*const ()`,没有实现 Send: ```rust pub struct Receiver<'a, T> {