Optimize translation with review.

This commit is contained in:
ic3w1ne
2025-08-14 21:10:16 +08:00
committed by GitHub
parent 21b2c1e198
commit 1338660a81

View File

@@ -40,9 +40,9 @@ translation_contributors = []
[_multitasking_]: https://en.wikipedia.org/wiki/Computer_multitasking
虽然看起来所有任务都在同时运行,但实际上单个 CPU 核心一次只能执行单个任务。为了制造任务同时运行的假象,操作系统会在活动任务之间快速切换,使每个任务都能取得一点进展。由于计算机运行速度极快,我们大多数时候都不会注意到这些切换。
虽然看起来所有任务都在同时运行,但实际上单个 CPU 核心一次只能执行单个任务。为了制造任务同时运行的假象,操作系统会在活动任务之间快速切换,使每个任务都能被执行到。由于计算机运行速度极快,我们大多数时候都不会注意到这些切换。
单核 CPU 一次只能执行一个任务,而多核 CPU 能够以真正并行的方式运行多个任务。例如,一个 8 核 CPU 可以同时运行 8 个任务。我们将在后续文章中介绍如何设置多核 CPU。本文中为了简单起见我们将重点讨论单核。值得注意的是所有的多核 CPU 都是从只有一个激活的核心开始的,所以我们现在可以将它们视为单核 CPU 来处理)。
单核 CPU 一次只能执行一个任务,而多核 CPU 能够以真正并行的方式运行多个任务。例如,一个 8 核 CPU 可以同时运行 8 个任务。我们将在后续文章中介绍如何设置多核 CPU。本文中为了简单起见我们将重点讨论单核。值得注意的是所有的多核 CPU 都是从个激活的核心启动的,所以我们现在可以先处理单核 CPU 的情况)。
多任务处理有两种形式_协作式多任务处理_ 要求任务定期主动让出对 CPU 的控制权以便其他任务能够运行。_抢占式多任务处理_ 利用操作系统在任意时间点强制暂停线程的能力实现切换线程的功能。在下文中,我们将更详细地探讨这两种多任务处理形式,并讨论它们各自的优势和缺点。
@@ -93,13 +93,13 @@ translation_contributors = []
#### 保存状态
由于任务自行定暂停点,它们不需要操作系统来保存其状态。相反,它们可以在暂停自己前精确保存恢复所需的状态,这通常会带来更好的性能表现。例如,一个刚刚完成复杂计算的任务可能只需要保存最终结果,而不再需要中间过程。
由于任务自行定暂停点,它们不需要操作系统来保存其所有的状态,而是自行在暂停前精确保存恢复所需的状态,这通常会带来更好的性能表现。例如,一个刚刚完成复杂计算的任务可能只需要保存最终结果,而不再需要中间过程。
协作式多任务处理的编程语言级实现甚至能够在暂停前保存调用栈的必要部分。例如Rust 的 async/await 实现会将所有仍被需要的局部变量存储在一个自动生成的结构体中(如后文所示)通过在暂停前保存调用栈的相关部分,所有任务可以共享单个调用栈,这使得每个任务的内存消耗大幅降低。从而实现创建任意数量的协作式任务并且不会耗尽内存。
#### 讨论
协作式多任务处理的缺点在于,一个不配合的任务可能会长时间占用处理器资源。因此,恶意或有缺陷的任务可能会阻止其他任务运行,并且会拖慢甚至阻塞整个系统。因此,协作式多任务处理应仅在所有任务都会协作的情况下使用。让操作系统依赖于任意用户级程序的协作并不是一个好主意。
协作式多任务处理的缺点在于,一个不愿意主动暂停的任务可能会长时间占用处理器资源。比如,恶意或有缺陷的任务可能会阻止其他任务运行,并且会拖慢甚至阻塞整个系统。因此,协作式多任务处理应仅在确保所有任务都会协作的情况下使用。让操作系统依赖于任意用户级程序的协作并不是一个好主意。
然而,协作式多任务处理在性能和内存方面的显著优势,使其成为适合在程序内部使用的好方法,特别是与异步操作结合使用。操作系统内核作为与异步硬件交互的性能关键程序,采用协作式多任务处理似乎是一种实现并发的理想方式。
@@ -229,7 +229,7 @@ fn file_len() -> impl Future<Output = usize> {
通过这个 `string_len` 函数,我们无需等待就能计算异步字符串的长度。由于该函数再次返回一个 `Future` ,调用者无法直接使用返回值,而是需要再次使用组合器函数。这样一来,整个调用链就变成了异步的,我们可以在某些节点(例如 main 函数中)高效地同时等待多个 future。
由于手动编写组合器函数较为困难,它们通常由库提供。虽然 Rust 标准库本身尚未提供组合器函数,但半官方的(且兼容 no_std 的)[`futures`] crate 提供了这些功能。其 [`FutureExt`] trait 提供了诸如 [`map`] 或 [`then`] 等高级组合器方法,可用于通过任意闭包来操作结果。
由于手动编写组合器函数较为困难,它们通常由库提供。虽然 Rust 标准库本身尚未提供组合器函数,但半官方的(且兼容 no_std 的)[`futures`] crate 提供了这些功能。其 [`FutureExt`] trait 提供了诸如 [`map`] 或 [`then`] 等高级组合器方法,可使用任意闭包来操作结果。
[`futures`]: https://docs.rs/futures/0.3.4/futures/
[`FutureExt`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html
@@ -360,7 +360,7 @@ struct EndState {}
_"Waiting on bar.txt"_ 状态包含用于后续 `bar.txt` 准备就绪时字符串拼接的 `content` 变量。它还存储了一个 `bar_txt_future` ,用于表示正在加载中的 `bar.txt`
该结构体不再包含 `min_len` 变量,因为在 `content.len()` 比较之后就不再需要它。在 _"end"_ 状态,不会存储任何变量,因为函数已经运行完
该结构体不再包含 `min_len` 变量,因为在 `content.len()` 比较之后就不再需要它。在 _"end"_ 状态,不会存储任何变量,因为函数已经运行完
请注意,这只是编译器可能生成的代码示例。结构体名称和字段布局的实现细节可能会有所不同。
@@ -529,7 +529,7 @@ struct WaitingOnWriteState {
}
```
我们需要同时 `array` 数组和 `element` 变量,因为 `element` 对于返回值是必需的,而 `array``element` 引用。由于 `element` 是一个引用,它存储了一个 _指针_ (即内存地址)指向被引用的元素。这里我们以 `0x1001c` 为例。实际上,它就是 `array` 字段最后一个元素的地址,因此这取决于结构体在内存中的位置。具有这种内部指针的结构体被称为 _自引用结构体_ _self-referential_ ),因为它们通过其中某个字段引用了自身。
我们需要同时保存 `array` 数组和 `element` 变量,因为 `element` 对于返回值是必需的,而 `array``element` 引用。由于 `element` 是一个引用,它存储了一个 _指针_ (即内存地址)指向被引用的元素。这里我们以 `0x1001c` 为例。实际上,它就是 `array` 字段最后一个元素的地址,因此这取决于结构体在内存中的位置。具有这种内部指针的结构体被称为 _自引用结构体_ _self-referential_ ),因为它们通过其中某个字段引用了自身。
#### 自引用结构体的问题
@@ -549,7 +549,7 @@ struct WaitingOnWriteState {
* **移动时更新指针:**其理念是每次结构体在内存中移动时都更新内部指针,从而保持有效。遗憾的是,这种方法需要对 Rust 进行大量修改,这可能导致巨大的性能损失。原因是需要某种运行时机制来跟踪所有结构体的字段类型并在每次移动操作时检查是否需要更新指针。
* **存储偏移量而非自引用:**为避免更新指针,编译器可以尝试将自引用存储为相对于结构体起始位置的偏移量。例如,上述 `WaitingOnWriteState` 结构体中的 `element` 字段可以存储为一个值为 8 的 `element_offset` 字段,因为引用点指向的数组元素在结构体起始位置之后 8 字节处。由于偏移量结构体被移动时保持不变,没有字段需要更新。这种方法的问题在于需要编译器检测所有自引用。这在编译时无法实现,因为引用的值可能取决于用户输入,因此就又需要一个运行时系统来分析引用并正确创建状态结构体。这不仅会导致运行时开销,还会影响某些编译器优化,从而再次造成较大的性能损失。
* **存储偏移量而非自引用:**为避免更新指针,编译器可以尝试将自引用存储为相对于结构体起始位置的偏移量。例如,上述 `WaitingOnWriteState` 结构体中的 `element` 字段可以存储为一个值为 8 的 `element_offset` 字段,因为引用点指向的数组元素在结构体起始位置之后 8 字节处。由于偏移量结构体被移动时保持不变,没有字段需要更新。这种方法的问题在于需要编译器检测所有自引用。这在编译时无法实现,因为引用的值可能取决于用户输入,因此就又需要一个运行时系统来分析引用并正确创建状态结构体。这不仅会导致运行时开销,还会影响某些编译器优化,从而再次造成较大的性能损失。
* **禁止移动结构体:**如上所述,只有在内存中移动结构体时才会出现悬垂指针。通过完全禁止对自引用结构体的移动操作就可以避免这个问题。这种方法的最大优势在于它能够在类型系统层面实现,无需额外的运行时开销。缺点是它将处理可能移动的自引用结构体的责任交给了程序员。
@@ -587,7 +587,7 @@ struct SelfReferential {
当我们在 playground 上执行这段代码时,可以看到堆值的地址与其内部指针是相等的,这意味着 `self_ptr` 字段是一个有效的自引用。由于 `heap_value` 变量仅是一个指针,移动它(例如传递给函数)并不会改变结构体自身的地址,因此即使指针被移动,`self_ptr` 仍保持有效。
然而,仍有一种方式可以破坏这个示例:我们可以从 `Box<T>` 移出或替换其内容:
然而,仍有一种方式可以破坏这个示例:我们可以从 `Box<T>` 将结构体移出或替换其内容:
```rust
let stack_value = mem::replace(&mut *heap_value, SelfReferential {
@@ -646,7 +646,7 @@ let mut heap_value = Box::pin(SelfReferential {
除了将 `Box::new` 改为 `Box::pin` 外,我们还需要在结构体初始化器中添加新的 `_pin` 字段。由于 `PhantomPinned` 是零大小类型,我们只要有其类型名称即可完成初始化。
当我们现在[尝试运行调整后的示例](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=961b0db194bbe851ff4d0ed08d3bd98a)时,会发现它不再有效
当我们现在[尝试运行调整后的示例](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=961b0db194bbe851ff4d0ed08d3bd98a)时,会发现它会报错
```
error[E0594]: cannot assign to data in a dereference of `std::pin::Pin<std::boxed::Box<SelfReferential>>`
@@ -894,7 +894,7 @@ impl SimpleExecutor {
}
```
该结构体包含一个类型为 [`VecDeque`] 的 `task_queue` 字段,其本质上是一个向量,允许在两端进行推入和弹出操作。采用这种类型的初衷是我们可以使用 `spawn` 方法在结尾插入新的任务,并从开头弹出下一个任务用于执行。这样子,我们就得到了一个简单的 [FIFO 队列][FIFO queue] _"first in, first out"_
该结构体包含一个类型为 [`VecDeque`] 的 `task_queue` 字段,其本质上是一个 Vec,允许在两端进行推入和弹出操作。采用这种类型的初衷是我们可以使用 `spawn` 方法在结尾插入新的任务,并从开头弹出下一个任务用于执行。这样子,我们就得到了一个简单的 [FIFO 队列][FIFO queue] _"first in, first out"_
[`VecDeque`]: https://doc.rust-lang.org/stable/alloc/collections/vec_deque/struct.VecDeque.html
[FIFO queue]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)