Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions 2_Atomics.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ fn get_x() -> u64 {

因为我们期待 x 是常量,那么谁赢得比赛并不重要,因为无论如何结果都是一样。依赖于 `calculate_x()` 会花费多少时间,这可能非常好或者很糟糕。

如果 `calculate_x()` 预计花费很长时间,则最好在第一个线程仍在初始化 X 时等待线程,以避免不必要的浪费处理器时间。你可以使用一个条件变量或者线程阻塞([第一章“等待-阻塞和条件变量”](./1_Basic_of_Rust_Concurrency.md#等待-阻塞park和条件变量))来实现这个,但是对于一个小例子来说,这很快将变得复杂。Rust 标准库通过 `std::sync::Once` 和 `std::sync::OnceLock` 正是此功能,所以通常这些不需要由你自己实现。
如果 `calculate_x()` 预计花费很长时间,则最好在第一个线程仍在初始化 X 时等待线程,以避免不必要的浪费处理器时间。你可以使用一个条件变量或者线程阻塞([第一章“等待-阻塞和条件变量”](./1_Basic_of_Rust_Concurrency.md#等待-阻塞park和条件变量))来实现这个,但是对于一个小例子来说,这很快将变得复杂。Rust 标准库通过 `std::sync::Once` 和 `std::sync::OnceLock` 提供了此功能,所以通常这些不需要由你自己实现。

## 获取并修改操作

Expand Down Expand Up @@ -224,7 +224,7 @@ fetch_add 操作从 100 增加到 123,但是返回给我们还是旧值 100。

来自这些操作的返回值并不总是相关的。如果你仅需要将操作用于原子值,但是值本身并没有用,那么你可以忽略该返回值。

需要记住的一个重要的事情是,fetch_add 和 fetch_sub 为溢出实现了包装行为。将值增加超过最大可表示值将包裹起来,并导致最小可表示值。这与常规整数上的增加和减少行为是不同的,后者调试模式下的溢出将 panic。
需要记住的一个重要的事情是,fetch_add 和 fetch_sub 为溢出实现了环绕(wrapping)行为。将值增加超过最大可表示值将导致环绕到最小可表示值。这与常规整数上的增加和减少行为是不同的,后者在调试模式下的溢出将 panic。

在[“比较并交换操作”](#比较并交换操作)中,我们将看见如何使用溢出检查做原子加法。

Expand All @@ -236,7 +236,7 @@ fetch_add 操作从 100 增加到 123,但是返回给我们还是旧值 100。

我们可以为每个线程使用单独的 AtomicUsize 并且在主线程加载它们并进行汇总,但是更简单的解决方案是,使用单个 AtomicUsize 去跟踪所有线程处理项的总数。

为了使其工作,我们不再使用 store 方法,因为这会覆盖其他线程的进度。而是,我们可以使用原子加操作为每个处理项之后增加计数器
为了使其工作,我们不再使用 store 方法,因为这会覆盖其他线程的进度。而是,我们可以使用原子自增操作在每个处理项之后增加计数器

```rust
fn main() {
Expand Down Expand Up @@ -268,15 +268,15 @@ fn main() {

一些东西已经改变。更重要地是,我们现在产生了 4 个后台线程而不是 1 个,并且使用 fetch_add 而不是 store 去修改 `num_done` 原子变量。

更巧妙地是,我们现在对后台线程使用一个 move 闭包,并且 num_done 现在是一个引用。这与我们使用 fetch_add 无关,而是我们如何在循环中产生 4 个线程有关。此闭包捕获 t,以了解它是 4 个线程中的哪一个,因此是否从项目 0、25、50 75 开始。没有 move 关键字,闭包将尝试通过引用捕获 t。这是不允许的,因为它仅在循环期间短暂存在。
更巧妙地是,我们现在对后台线程使用一个 move 闭包,并且 num_done 现在是一个引用。这与我们使用 fetch_add 无关,而是我们如何在循环中产生 4 个线程有关。此闭包捕获 t,以了解它是 4 个线程中的哪一个,从而确定是从项目 0、25、50 还是 75 开始。没有 move 关键字,闭包将尝试通过引用捕获 t。这是不允许的,因为它仅在循环期间短暂存在。

由于 move 闭包,它移动(或复制)它的捕获而不是借用它们,这使它得到 t 的复制。因为它也捕获 num_done,我们已经改变该变量为一个引用,因为我们仍然想要借用相同的 AtomicUsize。注意,原子类型并没有实现 Copy trait,所以如果我们尝试移动一个到多个线程,我们将得到错误。
由于 move 闭包,它移动(或复制)它的捕获而不是借用它们,这使它得到 t 的复制。因为它也捕获 num_done,我们已经改变该变量为一个引用,因为我们仍然想要借用相同的 AtomicUsize。注意,原子类型并没有实现 Copy trait,所以如果我们尝试移动一个原子类型的变量到多个线程,我们将得到错误。

撇开闭包的微妙不谈,在这里使用 fetch_add 更改是非常简单的。我们并不知道线程将以哪种顺序增加 num_done,但由于加法是原子的,我们并不担心任何事情,并且当所有线程完成,可以确信它将是 100。

### 示例:统计数据

继续通过原子报道其他线程正在做什么的概念,让我们拓展我们的示例,也可以收集和报道一些处理项目所花费的统计数据
继续通过原子报道其他线程正在做什么的概念,让我们拓展我们的示例,也可以收集和报道一些关于处理项目所花费时间的统计数据

在 num_done 旁边,我们增加了两个原子变量 `total_time` 和 `max_time`,以便跟踪处理项目所花费的时间。我们将使用这些报道平均和峰值处理时间。

Expand Down Expand Up @@ -373,7 +373,7 @@ fn allocate_new_id() -> u32 {

有三个通常的方式解决这个问题。第一种是不使用 panic,而是完全在溢出时终止进程。`std::process::abort` 函数将终止整个函数,排除任何继续调用我们函数的可能性。尽管中断进程可能需要短暂的时间,但是函数仍然可能通过其它线程调用,但在程序真正的终止之前,发生数十亿次调用的机会可以忽略不计。

事实上,在标准库中的 `Arc::clone()` 溢出检查时这么实现的,以防你在某种方式下克隆 `isize::MAX` 次。在 64 位计算机上,这需要上百年的时间,但如果 isize 只有 32 位,这仅需要几秒钟。
事实上,在标准库中的 `Arc::clone()` 溢出检查是这么实现的,以防你在某种方式下克隆 `isize::MAX` 次。在 64 位计算机上,这需要上百年的时间,但如果 isize 只有 32 位,这仅需要几秒钟。

处理溢出的第二种方法是使用 fetch_sub 在 panic 之前再次减少计数器,就像这样:

Expand All @@ -391,7 +391,7 @@ fn allocate_new_id() -> u32 {

当多个线程在相同时间执行这个函数,计数器仍然有可能在非常短暂的时间超过 100,但这受到活动线程数量的限制。合理地假设是永远不会有数十亿个激活的线程,并非所有线程都在 fetch_add 和 fetch_sub 之间的短暂时间内同时执行相同的函数。

这就是处理标准库 `thread::scope` 实现中运行线程数量溢出的方式
这就是标准库 `thread::scope` 实现中处理运行线程数量溢出的方式

第三种处理溢出的方式可以说是唯一正确的方式,因为如果它溢出,它完全可以阻止加操作发生。然而,我们不能使用迄今为止看到的原子操作实现这一点。为此,我们需要比较并交换操作,接下来我们将探索。

Expand Down Expand Up @@ -460,7 +460,7 @@ fn increment(a: &AtomicU32) {

> 如果原子变量从某个值更改 A 到 B,并且在加载操作之后,返回 A,但在 compare_exchange 操作之前,它仍然会成功,即使原子变量在此期间被更改(并且更改回来)。在很多示例中,就像在我们的加法示例中一样,这并不是一个问题。然而,有几种算法,通常涉及原子指针,这被称为 ABA 问题。

在 `compare_exchange` 旁边,有一个名为 `compare_exchange_weak` 的类似方法。区别是 weak 版本有时可能仍然不变的值并且返回 Err,即使原子值匹配期待值。在某些平台,这个方法可以更有效地实现,并且对于虚假的比较和交换失败的后果不重要的情况下,比如上面的递增函数,应该优先使用它。在[第七章节](./7_Understanding_the_Processor.md),我们将深入研究底层细节,以找出为什么 weak 版本会更有效。
在 `compare_exchange` 旁边,有一个名为 `compare_exchange_weak` 的类似方法。区别是 weak 版本有时可能仍然不变的值并且返回 Err,即使原子值匹配期待值。在某些平台,这个方法可以更有效地实现,并且对于虚假的比较并交换失败的后果不重要的情况下,比如上面的递增函数,应该优先使用它。在[第七章节](./7_Understanding_the_Processor.md),我们将深入研究底层细节,以找出为什么 weak 版本会更有效。

### 示例:没有溢出的 ID 分配

Expand Down Expand Up @@ -488,7 +488,7 @@ fn allocate_new_id() -> u32 {
<h2 style="text-align: center;">Fetch-Update</h2>
<p>原子类型有一个名为 <code>fetch_update</code> 的方便方法,用于比较并交换循环模式。它相当于加载操作,然后就是重复计算和 <code>compare_exchange_weak</code> 的循环,就像我们上面做的那样。</p>

<p>使用它,我们可以使用一行诗心啊我们的 allocate_new_id:</p>
<p>使用它,我们可以使用一行实现我们的 allocate_new_id:</p>

<pre>
NEXT_ID.fetch_update(Relaxed, Relaxed,
Expand Down Expand Up @@ -530,7 +530,7 @@ fn get_key() -> u64 {
3. 如果我们将 0 换成新密钥,我们将返回新生成的密钥。`get_key()` 的新调用将返回现在存储在 KEY 中的相同新密钥。
4. 如果我们输给了另一个初始化 KEY 的线程,我们忘记我们的新密钥,而是使用来自 KEY 的密钥。

这是一个很好的例子,在这里 `compare_exchange` 比 `weak` 变体更合适。我们不会在循环中运行比较和交换操作,如果操作虚假失败,我们不想返回 0。
这是一个很好的例子,在这里 `compare_exchange` 比 `weak` 变体更合适。我们不会在循环中运行比较并交换操作,如果操作虚假失败,我们不想返回 0。

正如[“示例:惰性初始化”](#示例惰性初始化)中提到的,如果 `generate_random_key()` 需要大量时间,那么在初始化期间阻塞线程可能更有意义,以避免可能花费时间生成不会使用的密钥。Rust 标准库通过 `std::sync::Once` 和 `std::sync::OnceLock` 提供此类功能。

Expand All @@ -542,7 +542,7 @@ fn get_key() -> u64 {
* 当涉及多个变量时,原子操作的相对顺序是棘手的。更多细节,请看[第三章](./3_Memory_Ordering.md)。
* 简单的 load 和 store 操作非常适合非常简单的基本线程间通信,例如停止标识和状态报道。
* 我们可以使用竞争条件[^2]来惰性始化,而不会引发数据竞争[^3]。
* 获取并修改操作允许进行一小组基本的原子修改操作,当多个线程同时修改同一个院子变量时,非常有用。
* 获取并修改操作允许进行一小组基本的原子修改操作,当多个线程同时修改同一个原子变量时,非常有用。
* 原子加法和减法在溢出时会默默地进行环绕(wrap around)操作。
* 比较并交换操作是最灵活和通用的,并且是任意其它原子操作的基石。
* *weak* 版本比较并交换稍微更有效。
Expand Down