Skip to content
Merged
Show file tree
Hide file tree
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
8 changes: 4 additions & 4 deletions 10_Ideas_and_Inspiration.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

就像计算机科学中的其他问题一样,该问题也可以通过增加间接的层来解决。你可以使用原子变量去存储一个指向它的指针,而不是结构体本身。这仍然不允许你以原子地方式修改整个结构体,但它允许你以原子地方式替换整个结构体,这差不多。

这种模式通常称为 `RCU`,代表“读取、复制、更新”,这些替换数据所需要的步骤。读取指针后,可以将结构体复制进新的分配,无需担心其他线程即可进行修改。准备就绪后,可以使用比较并交换操作([第二章节的“比较并交换”操作](./2_Atomics.md#比较并交换操作))来更新原子指针,如果没有其他线程在此期间替换数据,这将成功。
这种模式通常称为 `RCU`,代表“读取、复制、更新”,这些替换数据所需要的步骤。读取指针后,可以将结构体复制进新分配的内存,无需担心其他线程即可进行修改。准备就绪后,可以使用比较并交换操作([第二章节的“比较并交换”操作](./2_Atomics.md#比较并交换操作))来更新原子指针,如果没有其他线程在此期间替换数据,这将成功。

![ ](https://github.com/fwqaaq/Rust_Atomics_and_Locks/raw/main/picture/raal_10in02.png)

Expand All @@ -54,9 +54,9 @@

> 为了保持简单,你可以使用常规的 mutex 来避免并发的修改。这样,读仍然是一个无锁操作,但是你不需要担心处理并发修改。

从链表列表中分离元素后,你将遇到与之前相同的问题:它会等待,直到你解除分配(或者以其他方式宣称所有权)。在这种情况下,我们讨论的基本的 RCU 模式的相同解决方案也有效。
从链表列表中分离元素后,你将遇到与之前相同的问题:它会等待,直到你释放分配的内存(或者以其他方式宣称所有权)。在这种情况下,我们讨论的基本的 RCU 模式的相同解决方案也有效。

总的来说,你可以基于原子指针上的比较并交换操作,构建各种精心设计的无锁数据结构,但是你将总是需要一个好的策略来释放分配或者以其他方式收回分配的所有权
总的来说,你可以基于原子指针上的比较并交换操作,构建各种精心设计的无锁数据结构,但是你将总是需要一个好的策略来释放分配的内存或者以其他方式收回分配内存的所有权

进一步阅读:

Expand All @@ -73,7 +73,7 @@

![ ](https://github.com/fwqaaq/Rust_Atomics_and_Locks/raw/main/picture/raal_10in04.png)

有很多可能的变体。队列可能由它自己的锁位保护,或者也可以实现为(部分地)无锁结构。元素不必在堆上分配,而可以时等待的线程的局部变量。队列可以是一个双向链表,不仅包含指向下一个元素的指针,同时也包含指向前一个元素。第一个元素也包含一个指向最后元素的指针,以便高效地在末尾追加一个元素。
有很多可能的变体。队列可能由它自己的锁位保护,或者也可以实现为(部分地)无锁结构。元素不必在堆上分配,而可以是等待的线程的局部变量。队列可以是一个双向链表,不仅包含指向下一个元素的指针,同时也包含指向前一个元素。第一个元素也包含一个指向最后元素的指针,以便高效地在末尾追加一个元素。

这种模式仅允许使用可以用于阻塞和唤醒单个线程的方式(例如 `parking`)来高效地锁定原语。

Expand Down
14 changes: 7 additions & 7 deletions 1_Basic_of_Rust_Concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ thread::spawn(move || dbg!(x));

为了确保共享数据能够 drop 和释放内存,我们不能完全放弃它的所有权。相反,我们可以*分享所有权*。通过跟踪所有者的数量,我们确保仅当没有所有者时,才会丢弃该值。

Rust 标准库通过 `std::rc::Rc` 类型提供了该功能,它是“引用计数”(reference counted)的缩写。它与 `Box` 非常类似,唯一的区别是克隆它将不会分配任何新内存,而是增加存储在包含值旁边的计数器。原始的 `Rc` 和克隆的 `Rc` 将引用相同的内存分配;它们*共享所有权*。
Rust 标准库通过 `std::rc::Rc` 类型提供了该功能,它是“引用计数”(reference counted)的缩写。它与 `Box` 非常类似,唯一的区别是克隆它将不会分配任何新内存,而是增加存储在包含值旁边的计数器。原始的 `Rc` 和克隆的 `Rc` 将引用相同分配的内存;它们*共享所有权*。

```rust
use std::rc::Rc;
Expand All @@ -275,7 +275,7 @@ let b = a.clone();
assert_eq!(a.as_ptr(), b.as_ptr()); // Same allocation!
```

drop 一个 `Rc` 将减少计数。只有最后一个 `Rc`,计数器下降到 0,才会是 drop 和释放分配内存中所包含的数据
drop 一个 `Rc` 将减少计数。只有最后一个 `Rc`,计数器下降到 0,才会是 drop 且释放分配内存中所包含的数据

如果我们尝试去发送一个 Rc 到另一个线程,然而,我们将造成以下的编译错误:

Expand All @@ -286,7 +286,7 @@ error[E0277]: `Rc` cannot be sent between threads safely
| ^^^^^^^^^^^^^^^
```

事实证明,`Rc` 不是*线程安全*的(详见,[线程安全:Send 和 Sync](#线程安全send-和-sync))。如果多个线程有相同内存分配的 `Rc`,那么它们可能尝试同时修改引用计数,这可能产生不可预测的结果。
事实证明,`Rc` 不是*线程安全*的(详见,[线程安全:Send 和 Sync](#线程安全send-和-sync))。如果多个线程有相同分配内存的 `Rc`,那么它们可能尝试同时修改引用计数,这可能产生不可预测的结果。

然而,我们可以使用 `std::sync::Arc`,它代表“原子引用计数”。它与 `Rc` 相同,只是它保证了对引用计数的修改时不可分割的*原子*操作,因此可以安全地与多个线程使用。(详见第二章。)

Expand All @@ -300,9 +300,9 @@ thread::spawn(move || dbg!(a)); // 3
thread::spawn(move || dbg!(b)); // 3
```

1. 我们在新的内存分配中放置了一个一个数组,以及从一开始的引用计数器。
2. 克隆 Arc 增加引用计数到两个,并为我们提供相同的内存分配到第二个 Arc。
3. 两个 Arc 都获取到自己的 Arc,通过 Arc 它们可以获取共享数组。当它们 drop 它们的 Arc,两者都会减少引用计数。最后一个 drop 它的 Arc 的线程将看见计数器减少到 0,并且将是 drop 和取消内存分配数组的线程
1. 我们在新分配的内存中放置了一个一个数组,以及从一开始的引用计数器。
2. 克隆 Arc 增加引用计数到两个,并为我们提供第二个指向相同分配内存的 Arc。
3. 两个线程通过各自的 Arc 共访问享数组。当它们 drop 它们的 Arc,两者都会减少引用计数。最后一个 drop 它的 Arc 的线程将看见计数器减少到 0,并且将是 drop 和回收数组的内存

<div class="box">
<h2 style="text-align: center;">命名克隆</h2>
Expand Down Expand Up @@ -867,7 +867,7 @@ Condvar 的缺点是,它仅能与 Mutex 一起工作,对于大多数用例
* 多线程可以并发地运行在相同程序并且可以在任意时间生成。
* 当主线程结束,主程序结束。
* 数据竞争是未定义行为,它会由 Rust 的类型系统完全地组织(在安全的代码中)。
* 常规的线程可以像程序运行一样长时间,并且因此只能借用 `'static` 数据。例如静态变量和泄漏分配
* 常规的线程可以像程序运行一样长时间,并且因此只能借用 `'static` 数据。例如静态变量和泄漏分配的内存
* 引用计数(Arc)可以用于共享所有权,以确保只要有一个线程使用它,数据就会存在。
* 作用域线程用于限制线程的生命周期,以允许其借用非 `'static` 数据,例如作用域变量。
* `&T` 是*共享引用*。`&mut T` 是*独占引用*。常规类型不允许通过共享引用可变。
Expand Down
2 changes: 1 addition & 1 deletion 3_Memory_Ordering.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ fn get_data() -> &'static Data {

如果我们以 acquire-load 操作从 PTR 得到的指针是非空的,我们假设它指向已初始化的数据,并构建对该数据的引用。

然而,如果它仍然为空,我们会生成新数据,并使用 `Box::new` 将其存储在新分配内存中。然后,我们使用 `Box::into_raw` 将此 `Box` 转换为原始指针,因此我们可以尝试使用「比较并交换」操作将其存储到 PTR 中。如果另一个线程赢得初始化竞争,`compare_exchange` 将失败,因为 PTR 不再是空的。如果发生这种情况,我们将原始指针转回 Box,使用 `drop` 来释放分配的内存,避免内存泄漏,并继续使用另一个线程存储在 PTR 中的指针。
然而,如果它仍然为空,我们会生成新数据,并使用 `Box::new` 将其存储在新分配的内存中。然后,我们使用 `Box::into_raw` 将此 `Box` 转换为原始指针,因此我们可以尝试使用「比较并交换」操作将其存储到 PTR 中。如果另一个线程赢得初始化竞争,`compare_exchange` 将失败,因为 PTR 不再是空的。如果发生这种情况,我们将原始指针转回 Box,使用 `dro`p 来释放分配的内存,避免内存泄漏,并继续使用另一个线程存储在 PTR 中的指针。

在最后的不安全块中,关于安全性的注视表明我们的假设是指它指向的数据已经被初始化。注意,这包括对事情发生顺序的假设。为了确保我们的假设成立,我们使用 release 和 acquire 内存顺序来确保初始化数据实际上在创建对其的引用之前已经发生。

Expand Down
10 changes: 5 additions & 5 deletions 5_Building_Our_Own_Channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ impl<T> Channel<T> {

> 记住,`Condvar::wait` 方法将在等待时解锁 Mutex,并在返回之前重新锁定它。因此,我们的 `receive` 函数将不会在等待时锁定 mutex。

尽管这个 channel 在使用上是非常灵活的,因为它允许任意数量的发送和接收线程,它的实现在很多情况下远非最佳。即使有大量的消息准备好被接收,任意的发送或者接收操作将短暂地阻塞任意其它的发送或者接收操作,因为它们必须都锁定相同的 mutex。如果 `VecDeque::push` 不得不增加 VecDeque 的容量,所有的发送和接收线程将不得不等待该线程完成重新容量的分配,这在某些情况下是不可接受的。
尽管这个 channel 在使用上是非常灵活的,因为它允许任意数量的发送和接收线程,它的实现在很多情况下远非最佳。即使有大量的消息准备好被接收,任意的发送或者接收操作将短暂地阻塞任意其它的发送或者接收操作,因为它们必须都锁定相同的 mutex。如果 `VecDeque::push` 不得不增加 VecDeque 的容量,所有的发送和接收线程将不得不等待该线程完成重新分配容量,这在某些情况下是不可接受的。

另一个可能不可取的属性是,该 channel 的队列可能会无限制地增长。没有什么能阻止发送者以比接收者更高的速度持续发送新消息。

Expand Down Expand Up @@ -374,7 +374,7 @@ impl<T> Receiver<T> {

为了实现这一点,我们需要为我们的 UnsafeCell 和 AtomicBool 找到一个位置。之前,我们仅有一个具有这些字段的结构体,但是现在我们有两个单独的结构体,每个结构体都可能存在更长的时间。

因为 sender 和 receiver 将需要共享这些变量的所有权,我们将使用 Arc([第一章“引用计数”](./1_Basic_of_Rust_Concurrency.md#引用计数))为我们提供引用计数的共享分配,我们将在其中存储共享的 Channel 对象。正如以下展示的,Channel 类型不必是公共的,因为它的存在是与用户无关的细节。
因为 sender 和 receiver 将需要共享这些变量的所有权,我们将使用 Arc([第一章“引用计数”](./1_Basic_of_Rust_Concurrency.md#引用计数))为我们提供引用计数的共享分配的内存,我们将在其中存储共享的 Channel 对象。正如以下展示的,Channel 类型不必是公共的,因为它的存在是与用户无关的细节。

```rust
pub struct Sender<T> {
Expand Down Expand Up @@ -454,7 +454,7 @@ impl<T> Drop for Channel<T> {
}
```

当 `Sender<T>` 或者 `Receiver<T>` 被 drop 时,`Arc<Channel<T>>` 的 Drop 实现将减少分配的引用计数。当 drop 到第二个时,计数达到 0,并且 `Channel<T>` 自身被 drop。这将调用我们上面的 Drop 实现,如果已发送但未收到消息,我们将 drop 该消息。
当 `Sender<T>` 或者 `Receiver<T>` 被 drop 时,`Arc<Channel<T>>` 的 Drop 实现将减少对共享分配的内存的引用计数。当 drop 到第二个时,计数达到 0,并且 `Channel<T>` 自身被 drop。这将调用我们上面的 Drop 实现,如果已发送但未收到消息,我们将 drop 该消息。

让我们尝试它:

Expand Down Expand Up @@ -505,9 +505,9 @@ note: this function takes ownership of the receiver `self`, which moves `sender`

不得不在安全性、便利性、灵活性、简单性和性能之间进行权衡是不幸的,但有时是不可避免的。Rust通常致力于在这些方面取得最佳表现,但有时为了最大化某个方面的优势,我们需要在其中做出一些妥协。

## 借用以避免分配
## 借用以避免分配内存

我们刚刚基于 Arc 的 channel 实现的设计可以非常方便的使用——代价是一些性能,因为它得分配内存。如果我们想要优化效率,我们可以通过用户对共享的 Channel 对象负责来获取一些性能。我们可以强制用户去创建一个通过可以由 Sender 和 Receiver 借用的 Channel,而不是在幕后处理 Channel 的分配和所有权。这样,它们可以选择简单地放置 Channel 在局部变量中,从而避免分配内存的开销。
我们刚刚基于 Arc 的 channel 实现的设计可以非常方便的使用——代价是一些性能,因为它得分配内存。如果我们想要优化效率,我们可以通过用户对共享的 Channel 对象负责来获取一些性能。我们可以强制用户去创建一个通过可以由 Sender 和 Receiver 借用的 Channel,而不是在幕后处理 Channel 分配的内存和所有权。这样,它们可以选择简单地放置 Channel 在局部变量中,从而避免分配内存的开销。

我们将也在一定程度上牺牲简洁性,因为我们现在不得不处理借用和生命周期。

Expand Down
4 changes: 2 additions & 2 deletions 6_Building_Our_Own_Arc.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 第六章:构建我们自己的“Arc”

在[第一章“引用计数”](./1_Basic_of_Rust_Concurrency.md#引用计数)中,我们了解了 `std::sync::Arc<T>` 类型允许通过引用计数共享所有权。`Arc::new` 函数创建一个新分配的内存,就像 `Box::new`。然而,与 Box 不同的是,克隆 Arc 将共享原始的内存分配,而不是创建一个新的。只有当 Arc 和所有其他的克隆被 drop,共享分配的内存才会被 drop。
在[第一章“引用计数”](./1_Basic_of_Rust_Concurrency.md#引用计数)中,我们了解了 `std::sync::Arc<T>` 类型允许通过引用计数共享所有权。`Arc::new` 函数创建一个新分配的内存,就像 `Box::new`。然而,与 Box 不同的是,克隆 Arc 将共享原始分配的内存,而不是创建一个新的。只有当 Arc 和所有其他的克隆被 drop,共享分配的内存才会被 drop。

这种类型的实现所涉及的内存排序可能是非常有趣的。在本章中,我们将通过实现我们自己的 `Arc<T>` 将更多理论付诸实践。我们将开始一个基础的版本,然后将其扩展到支持循环结构的 *weak 指针*,并且最终将其优化为一个与标准库差不多的实现结束本章。

Expand All @@ -19,7 +19,7 @@ struct ArcData<T> {

接下来是 `Arc<T>` 结构体本身,它实际上仅是一个指向(共享的)`ArcData<T>` 的指针。

使用 `Box<ArcDate<T>>` 作为包装器,并使用标准的 Box 来处理 `ArcData<T>` 的内存分配可能很诱人。然而,Box 表示独占所有权,并不是共享所有权。我们不能使用引用,因为我们不仅要借用其他所有权的数据,并且它的生命周期(“直到此 Arc 的最后一个克隆被 drop”)无法直接表示为 Rust 的生命周期。
使用 `Box<ArcDate<T>>` 作为包装器,并使用标准的 Box 来处理 `ArcData<T>` 分配的内存可能很诱人。然而,Box 表示独占所有权,并不是共享所有权。我们不能使用引用,因为我们不仅要借用其他所有权的数据,并且它的生命周期(“直到此 Arc 的最后一个克隆被 drop”)无法直接表示为 Rust 的生命周期。

相反,我们将不得不使用指针,并手动处理分配内存以及所有权的概念。我们将使用 `std::ptr::NonNull<T>`,而不是 `*mut T` 或 `*const T`,它表示一个永远不会为空的指向 T 的指针。这样,使用 None 的空指针表示 `Option<Arc<T>>` 与 `Arc<T>` 的大小相同。

Expand Down
Loading