diff --git a/10_Ideas_and_Inspiration.md b/10_Ideas_and_Inspiration.md index b3b9da6..862b7c8 100644 --- a/10_Ideas_and_Inspiration.md +++ b/10_Ideas_and_Inspiration.md @@ -84,11 +84,11 @@ Windows SRW 锁([第8章中的“一个轻巧的读写锁”](./8_Operating_Sy * [关于 Windows SRW 锁的实现](https://oreil.ly/El8GA) * [基于队列的锁的 Rust 实现](https://oreil.ly/aFyg1) -## 基于 Parking 的锁 +## 基于阻塞的锁 为了创建一个尽可能小的高效 mutex,你可以通过将队列移动到全局的数据结构,在 mutex 自身只留下 1 或者 2 个位,来构建基于队列的锁的想法。这样,mutex 仅需要是一个字节。你甚至可以把它放置在一些未使用的指针位中,这允许非常细粒度的锁定,几乎没有其他额外的开销。 -全局的数据结构可以是一个 `HashMap`,将内存地址映射到等待该地址的 mutex 的线程队列。全局的数据结构通常叫做 `parking lot`,因为它是一组被 `park` 的线程合集。 +全局的数据结构可以是一个 `HashMap`,将内存地址映射到等待该地址的 mutex 的线程队列。全局的数据结构通常叫做 `parking lot`,因为它是一组被阻塞(`park`)的线程合集。  diff --git a/1_Basic_of_Rust_Concurrency.md b/1_Basic_of_Rust_Concurrency.md index 92f7ea3..72cc606 100644 --- a/1_Basic_of_Rust_Concurrency.md +++ b/1_Basic_of_Rust_Concurrency.md @@ -1,45 +1,888 @@ # 第一章:Rust 并发基础 +早在多核处理器司空见惯之前,操作系统就允许一台计算机运行多个程序。这是通过在进程之间快速切换来完成的,允许每个进程逐个地重重地取得一点进展。现在,几乎所有的电脑,甚至手机和手表都有着多核处理器,可以真正并行执行多个程序。 + +操作系统尽可能的将进程之间隔离,允许程序完全意识不到其他线程在做什么的情况下做自己的事情。例如,在不先询问操作系统内核的情况下,一个进程通常不能获取其他进程的内存,或者以任意方式与之通信。 + +然而,一个程序可以产生额外的*执行线程*作为*进程*的一部分。同一进程中的线程不会相互隔离。线程共享内存并且可以通过内存相互交互。 + +这一章节将阐述在 Rust 中如何产生线程,并且关于它们的所有基本概念,例如如何安全地在多个线程之间共享数据。本章中解释的概念是本书其余部分的基础。 + +> 如果你已经熟悉 Rust 中的这些部分,你可以随时跳过。然而,在你继续下一章节之前,请确保你对线程、内部可变性、Send 和 Sync 有一个好的理解,以及知道什么是互斥锁[^2]、条件变量[^1]以及线程阻塞(park)[^3]。 + ## Rust 中的线程 +每个程序都从一个线程开始:主(main)线程。该线程将执行你的 main 函数,并且如果需要,它可以用于产生更多线程。 + +在 Rust 中,新线程使用来自标准库的 `std::thread::spawn` 函数产生。它接受一个参数:新线程执行的函数。一旦该函数停止,将立刻返回。 + +让我们看一个示例: + +```rust +use std::thread; + +fn main() { + thread::spawn(f); + thread::spawn(f); + + println!("Hello from the main thread."); +} + +fn f() { + println!("Hello from another thread!"); + + let id = thread::current().id(); + println!("This is my thread id: {id:?}"); +} +``` + +我们产生两个线程,它们都将执行 f 作为它们的主函数。这两个线程将输出一个信息并且展示它们的*线程 id*,主线程也将输出它自己的信息。 + +
+ Hello fromHello from another thread! + another This is my threthreadHello fromthread id: ThreadId! + ( the main thread. + 2)This is my thread + id: ThreadId(3) +
+let a = a.clone(); 的语句在该作用域内重用相同的名称,同时在作用于外保留原始变量的可用性。
+
+ 通过在新的作用域(使用 {})中封装闭包,我们可以在将变量移动到闭包中之前,进行克隆,并不要重新命名它们。
+
+let a = Arc::new([1, 2, 3]);
+let b = a.clone();
+thread::spawn(move || {
+ dbg!(b);
+});
+dbg!(a);
+
+ Arc 克隆存活在相同作用域范围内。每个线程都有自己的克隆,只是名称不同。
+
+let a = Arc::new([1, 2, 3]);
+thread::spawn({
+ let a = a.clone();
+ move || {
+ dbg!(a);
+ }
+});
+dbg!(a);
+
+ Arc 的克隆存活在不同的生命周期范围内。我们可以在每个线程使用相同的名称。
+ get_unchecked 方法的小片段:
+
+
+let a = [123, 456, 789];
+let b = unsafe { a.get_unchecked(index) };
+
+ get_unchecked 方法给我们一个给定索引的切片元素,就像 a[index],但是允许变异器假设索引总是在边界,没有任何检查。
+
+ 这意味着,在代码片段中,由于 a 的长度是 3,编译器可能假设索引小雨 3。这使我们确保其假设成立。
+
+ 如果我们破坏了这个假设,例如,我们以等于 3 的索引运行,任何事情都可能发生。它可能导致读取 `a` 之后存储的任何内存内容。这可能导致程序崩溃。它可能会执行程序中完全无关的部分。它可能会引起各种糟糕的情况。
+
+ 或许令人惊讶的是,为定义行为可能“回到过去”,导致之前的代码出问题。要理解这种情况是如何发生的,想象我们上面的片段有一个 match 语句,如下:
+
+
+match index {
+ 0 => x(),
+ 1 => y(),
+ _ => z(index),
+}
+
+let a = [123, 456, 789];
+let b = unsafe { a.get_unchecked(index) };
+
+ 由于不安全的代码,允许编译器假设索引只有 0、1 或 2。从逻辑上讲,我们的 match 语句的最后分支仅会匹配到 2,因此 z 仅会调用为 z(2)。这个结论不仅可以优化匹配,还可以优化 z 本身。这可以扔掉代码中未使用的部分。
+
+ 如果我们以 3 的索引执行此设置,我们的程序试图去执行已优化的部分,导致完全地为定义行为,这还远在我们到达最后一行不安全的块之前。就像这样,未定义行为通过整个程序向后或者向前传播,通过非常意想不到的方式传播。
+
+ 当调用任何的不安全函数时,读它的文档并确保你完全理解它的安全需求:作为调用者,你需要坚持的假设,以避免未定义行为。
+Mutex> ,你可以在单个语句中锁定 mutex,将项推入 Vec,并且再次锁定 mutex:
+
+ list.lock().unwrap().push(1);+ + 任何更大表达式产生的临时值,例如通过
lock() 返回的 guard,将在语句结束后被 drop。尽管这似乎显而易见,但它导致了一个常见的问题,这通常涉及 match、if let 以及 while let 语句。以下是遇到该陷阱的示例:
+
+ if let Some(item) = list.lock().unwrap().pop() {
+ process_item(item);
+}
+
+ 如果我们的旨意就是锁定 list、弹出 item、释放 list 然后在释放 list 后处理 item,我们在这里犯了一个微妙而严重的错误。临时的 guard 直到完整的 if let 语句结束后才能被 drop,这意味着我们在处理 item 时不必要地持有锁。
+
+ 或许,意外地是,对于类似地 if 语句,这并不会发生,例如以下示例:
+
+ if list.lock().unwrap().pop() == Some(1) {
+ do_something();
+}
+
+ 在这里,临时的 guard 在 if 语句的主题执行之前就执行 drop 了。该原因是,通常 if 语句的条件总是一个布尔值,它并不能借用任何东西。没有理由将临时的生命周期从条件开始延长到语句的结尾。对于 if let 语句,情况可能并非如此。例如,如果我们使用 `front()`,而不是 `pop()`,项将会从 list 中借用,因此有必要保持 guard 存在。因为借用检查实际上只是一种检查,它并不会影响何时以及什么顺序 drop,所以即使我们使用了 pop(),情况仍然是相同的,尽管那并不是必须的。
+
+ 我们可以通过将弹出操作移动到单独的 let 语句来避免这种情况。然后在该语句的末尾放下 guard,在 if let 之前:
+
+ let item = list.lock().unwrap().pop();
+if let Some(item) = item {
+ process_item(item);
+}
+Mutex 数据包含它正在保护的数据。例如,在 C++ 中,std::mutex 并不包含着它保护的数据,甚至不知道它在保护什么。这意味着,用户有指责记住哪些数据由 mutex 保护,并且确保每次访问“受保护”的数据都锁定正确的 mutex。注意,当读取其它语言涉及到 mutex 的代码,或者与不熟悉 Rust 程序员沟通时,非常有用。Rust 程序员可能讨论关于“数据在 mutex 之中”,或者说“mutex 中包裹数据”这类话,这可能让只熟悉其它语言 mutex 的程序员感到困惑。
+
+ 如果你真的需要一个不包含任何内容的独立 mutex,例如,保护一些外部硬件,你可以使用 Mutex<()>。但即使是这种情况,你最好定义一个(可能 0 大小开销)的类型来与该硬件对接,并将其包裹在 Mutex 之中。这样,在与硬件交互之前,你仍然可以强制锁定 mutex。
+