Thread::id()
访问并且拥有 ThreadId 类型。除了复制 ThreadId 以及检查它们是否相等外,你什么也做不了。不能保证这些 ID 将会连续分配,并且每个线程的 ID 都会有所不同。
48 | std::io::Stdout::lock()
去确保输出没有被中断。println!()
表达式将等待直到任意并发的表达式运行完成后,再写入输出。如果不是这样,我们可能得到更多的交错输出:
105 |
106 | 107 | Hello fromHello from another thread! 108 | another This is my threthreadHello fromthread id: ThreadId! 109 | ( the main thread. 110 | 2)This is my thread 111 | id: ThreadId(3)112 |
std::thread::spawn
函数事实上仅是 std::thread::Builder::new().spawn().unwrap()
的简写。
std::thread::Builder
允许你在产生线程之前为新线程做一些配置。你可以使用它为新线程配置栈大小并给新线程一个名字。线程的名字是可以通过 std::thread::current().name()
获得,这将在 panic 消息中可用,并在监控和大多数调试工具中可见。
此外,Builder 的产生函数返回一个 std::io::Result
,允许你处理产生新线程失败的情况。如果操作系统内存不足,或者资源限制已经应用于你的程序,这是可能发生的。如果 std::thread::spawn
函数不能去产生一个新线程,它就会 panic。
在 Rust 1.0 之前,标准库有一个函数叫做 std::thread::scoped
,它将直接产生一个线程,就像 std::thread::spawn
。它允许无 'static
的捕获,因为它返回的不是 JoinHandle,而是当被丢弃时 join 到线程的 JoinGuard。任意的借用数据仅需要比这个 JoinGuard 存活得更久。只要 JoinGuard 在某个时候被丢弃,这似乎就是安全的。
就在 Rust 1.0 发布之前,人们慢慢发现它似乎不能保证某些东西一定被丢弃。有很多种方式不能丢弃它,例如创建一个引用计数节点的循环,可以忘记某些东西或者泄漏它。
227 | 228 |最终,在一些人提及的“泄漏启示录”中得到结论,(安全)接口的设计不能依赖假设对象总是在它们的生命周期结束后丢弃。泄漏一个对象可能会导致泄漏更多对象(例如,泄漏一个 Vec 将也导致泄漏它的元素),但它并不会导致未定义行为(undefind behavior)。因此,std::thread::scoped
将不再视为安全的并从标准库移除。此外,std::mem::forget
从一个不安全的函数升级到安全的函数,以强调忘记(或泄漏)总是一种可能性。
直到后来,在 Rust 1.63 中,添加了一个新的 std::thread::scope
功能,其新设计不依赖 Drop 来获得正确性。
如果给每个 Arc 的克隆取一个不同的名称,这可能使得代码变得混乱难以追踪。尽管每个 Arc 的克隆都是一个独立的对象,而给每个克隆赋予不同的名称也并不能很好地反映这一点。
325 | 326 |Rust 允许(并且鼓励)你通过定义有着新的名称的相同变量去遮蔽变量。如果你在同一作用域这么做,则无法再命名原始变量。但是通过打开一个新的作用域,可以使用类似 let a = a.clone();
的语句在该作用域内重用相同的名称,同时在作用于外保留原始变量的可用性。
通过在新的作用域(使用 {}
)中封装闭包,我们可以在将变量移动到闭包中之前,进行克隆,而不重新命名它们。
332 | let a = Arc::new([1, 2, 3]); 333 | let b = a.clone(); 334 | thread::spawn(move || { 335 | dbg!(b); 336 | }); 337 | dbg!(a); 338 |339 | Arc 克隆存活在同一作用域内。每个线程都有自己的克隆,只是名称不同。 340 |
343 | let a = Arc::new([1, 2, 3]); 344 | thread::spawn({ 345 | let a = a.clone(); 346 | move || { 347 | dbg!(a); 348 | } 349 | }); 350 | dbg!(a); 351 |352 | Arc 的克隆存活在不同的作用域内。我们可以在每个线程使用相同的名称。 353 |
类似 C、C++ 和 Rust 都有一套需要遵守的规则,以避免未定义行为。例如,Rust 的规则之一是,对任何对象的可变引用永远不可能超过一个。
401 | 402 |在 Rust 中,仅当使用 unsafe 代码块才能打破这些规则。“unsafe”并不意味着代码是错误的或者不安全的,而是编译器并没有为你验证你的代码是安全的。如果代码确实违法了这些规则,则称为不健全的(unsound)。
403 | 404 |允许编译器在不检查的情况下假设这些规则从未破坏。当破坏是,这将导致叫做未定义行为的问题,我们需要不惜一切代价去避免。如果我们允许编译器作出与实际不符的假设,那么它可能很容易导致关于代码不同部分更错误的结论,影响你整个程序。
405 | 406 |作为一个具体的例子,让我们看看在切片上使用 get_unchecked
方法的小片段:
let a = [123, 456, 789]; 409 | let b = unsafe { a.get_unchecked(index) };410 | 411 |
get_unchecked
方法给我们一个给定索引的切片元素,就像 a[index]
,但是允许编译器假设索引总是在边界,没有任何检查。
这意味着,在代码片段中,由于 a 的长度是 3,编译器可能假设索引小于 3。这使我们确保其假设成立。
414 | 415 |如果我们破坏了这个假设,例如,我们以等于 3 的索引运行,任何事情都可能发生。它可能导致读取 a 之后存储的任何内存内容。这可能导致程序崩溃。它可能会执行程序中完全无关的部分。它可能会引起各种糟糕的情况。
416 | 417 |或许令人惊讶的是,未定义行为甚至可以“时间回溯”,导致之前的代码出问题。要理解这种情况是如何发生的,想象我们上面的片段有一个 match 语句,如下:
418 | 419 |match index { 420 | 0 => x(), 421 | 1 => y(), 422 | _ => z(index), 423 | } 424 | 425 | let a = [123, 456, 789]; 426 | let b = unsafe { a.get_unchecked(index) };427 | 428 |
由于不安全的代码,编译器被允许假设 index 只有 0、1 或 2。它可能会逻辑的得出结论,我们的 match 语句的最后分支仅会匹配到 2,因此 z 仅会调用为 z(2)
。这个结论不仅可以优化匹配,还可以优化 z 本身。这可能包括丢弃代码中未使用的部分。
如果我们以 3 为 index 执行此设置,我们的程序可能会尝试执行被优化的部分,导致在我们到达最后一行的 unsafe 块之前就出现不可预测的行为。就像这样,未定义行为通过整个程序向后或者向前传播,而这往往是以非常出乎意料的方式发生。
431 | 432 |当调用任何的不安全函数时,仔细阅读其文档,确保你完全理解它的安全要求:作为调用者,你需要维持约定或前提条件,以避免未定义行为。
433 |尽管隐式丢弃 guard 解锁 mutex 很方便,但是它有时会导致微妙的意外。如果我们使用 let 语句授任 guard 一个名字(正如我们上面的示例),看它什么时候会被丢弃相对简单,因为局部变量定义在它们作用域的末尾。然而,正如上述示例所示,不明确地丢弃 guard 可能导致 mutex 锁定的时间超过所需时间。
723 | 724 |在不给它指定名称的情况下使用 guard 也是可能的,并且有时非常方便。因为 MutexGuard 保护数据的行为像独占引用,我们可以直接使用它,而无需首先为他授任一个名称。例如,你有一个 Mutex<Vec<i32>>
,你可以在单个语句中锁定 mutex,将项推入 Vec,并且再次锁定 mutex:
list.lock().unwrap().push(1);727 | 728 |
任何更大表达式产生的临时值,例如通过 lock()
返回的 guard,将在语句结束后被丢弃。尽管这似乎显而易见,但它导致了一个常见的问题,这通常涉及 match
、if let
以及 while let
语句。以下是遇到该陷阱的示例:
if let Some(item) = list.lock().unwrap().pop() { 731 | process_item(item); 732 | }733 | 734 |
如果我们的旨意就是锁定 list、弹出 item、解锁 list 然后在解锁 list 后处理 item,我们在这里犯了一个微妙而严重的错误。临时的 guard 直到完整的 if let
语句结束后才能被丢弃,这意味着我们在处理 item 时不必要地持有锁。
if
语句,这并不会发生,例如以下示例:
737 |
738 | if list.lock().unwrap().pop() == Some(1) { 739 | do_something(); 740 | }741 | 742 |
在这里,临时的 guard 在 if
语句的主体执行之前就已经丢弃了。该原因是,通常 if 语句的条件总是一个布尔值,它并不能借用任何东西。没有理由将临时的生命周期从条件开始延长到语句的结尾。对于 if let
语句,情况可能并非如此。例如,如果我们使用 front()
,而不是 pop()
,项将会从 list 中借用,因此有必要保持 guard 存在。因为借用检查实际上只是一种检查,它并不会影响何时以及什么顺序丢弃,所以即使我们使用了 pop()
,情况仍然是相同的,尽管那并不是必须的。
我们可以通过将弹出操作移动到单独的 let 语句来避免这种情况。然后在该语句的末尾放下 guard,在 if let
之前:
let item = list.lock().unwrap().pop(); 747 | if let Some(item) = item { 748 | process_item(item); 749 | }750 |
Rust 标准的 Mutex 和 RwLock 类型与你在其它语言(例如 C、C++)发现的看起来有一点不同。
771 | 772 |最大的区别是,Rust 的 Mutex<T>
数据包含它正在保护的数据。例如,在 C++ 中,std::mutex
并不包含着它保护的数据,甚至不知道它在保护什么。这意味着,用户有职责记住哪些数据由 mutex 保护,并且确保每次访问“受保护”的数据都锁定正确的 mutex。注意,当阅读其它语言涉及到 mutex 的代码,或者与不熟悉 Rust 程序员沟通时,非常有用。Rust 程序员可能讨论关于“数据在 mutex 之中”,或者说“mutex 中包装数据”这类话,这可能让只熟悉其它语言 mutex 的程序员感到困惑。
如果你真的需要一个不包含任何内容的独立 mutex,例如,保护一些外部硬件,你可以使用 Mutex<()>
。但即使是这种情况,你最好定义一个(可能 0 大小开销)的类型来与该硬件对接,并将其包装在 Mutex 之中。这样,在与硬件交互之前,你仍然可以强制锁定 mutex。
928 | 下一篇,第二章:Atomic 929 |
930 | 931 | [^1]:原子类型有一个名为 fetch_update
的简写方法,用于「比较并交换」循环模式。它相当于 load 操作,然后就是重复计算和 compare_exchange_weak
的循环,就像我们上面做的那样。
使用它,我们可以使用一行实现我们的 allocate_new_id:
516 | 517 |518 | NEXT_ID.fetch_update(Relaxed, Relaxed, 519 | |n| n.checked_add(1)).expect("too many IDs!")520 | 521 |
有关详细信息,请查看该方法的文档。
522 | 523 |我们不会在本书中使用 fetch_update
方法,因此我们可以专注于单个原子操作。
579 | 下一篇,第三章:内存排序 580 |
581 | 582 | [^1]:在使用 Relaxed 内存排序时,由于缺乏顺序保证,当操作在循环方式下相互依赖时,可能会导致理论上的复杂情况。
194 | 195 |为了演示,这里有一个人为的例子,两个线程从一个原子加载一个值,并将其存储在另一个原子中:
196 | 197 |static X: AtomicI32 = AtomicI32::new(0); 198 | static Y: AtomicI32 = AtomicI32::new(0); 199 | 200 | fn main() { 201 | let a = thread::spawn(|| { 202 | let x = X.load(Relaxed); 203 | Y.store(x, Relaxed); 204 | }); 205 | let b = thread::spawn(|| { 206 | let y = Y.load(Relaxed); 207 | X.store(y, Relaxed); 208 | }); 209 | a.join().unwrap(); 210 | b.join().unwrap(); 211 | assert_eq!(X.load(Relaxed), 0); // 可能失败? 212 | assert_eq!(Y.load(Relaxed), 0); // 可能失败? 213 | }214 | 215 |
似乎很容易得出 X 和 Y 的值不会是除 0 以外的任何东西的结论,因为 store 操作仅从这项相同的原子中加载值,而这些原子仅是 0。
216 | 217 |然而,如果我们严格遵循理论内存模型,我们必须面对循环推理,并得出可怕的结论,我们可能错了。事实上,内存模型在技术上允许出现这样的结果,即最终 X 和 Y 都是 37,或者任意其它的值,导致断言失败。
218 | 219 |由于缺乏顺序保证,这两个线程的 load 操作可能都看到另一个线程 store 操作的结果,允许按操作顺序循环:我们在 Y 中存储 37,因为我们从 X 加载了 37,X 存储到 X,因为我们从 Y 加载了 37,这是我们在 Y 中存储的值。
220 | 221 |幸运的是,这种凭空捏造值的可能性在理论模型中被普遍认为是一个 bug,而不需要你在实践中考虑。如何在不允许这种异常情况的情况下形式化 relaxed 内存排序还是一个未解决的问题。尽管这对于形式化验证来说可能是一个问题,让许多理论家夜不能寐,但是我们其他人可以放心地使用 relaxed,因为在实践中不会发生这种情况。
222 |当 acquire-load 操作观察 release-store 操作的结果时,就会形成 happens-before 关系。但那是什么意思?
292 | 293 |想象一下,两个线程都将一个 7 release-store 到相同的原子变量中,第三个线程从该变量中加载 7。第三个线程和第一个或者第二个线程有一个 happens-before 关系吗?这取决于它加载“哪个 7”:线程一还是线程二的。(或许一个不相关的 7)。这使我们得出的结论是,尽管 7 等于 7,但两个 7 与两个线程有一些不同。
294 | 295 |思考这个问题的方式是我们在“Relaxed 排序”中讨论的总修改顺序:发生在原子变量上的所有修改的有序列表。即使将相同的值多次写入相同的变量,这些操作中的每一个都以该变量的总修改顺序代表一个单独的事件。当我们加载一个值,加载的值与每个变量“时间线”上的特定点相匹配,这告诉我们我们可能会同步哪个操作。
296 | 297 |例如,如果原子总修改顺序是
298 | 299 |1. 初始化为 0
300 | 301 |2. Release-store 7(来自线程二)
302 | 303 |3. Release-store 6
304 | 305 |4. Release-store 7(来自线程一)
306 | 307 |然后,acquire-load 7 将与第二个线程的 release-store 或者最后一个事件的 release-store 同步。然而,如果我们之前(就 happens-before 关系而言)见过 6,我们知道我们看到的是最后一个 7,而不是第一个 7,这意味着我们现在与线程一有 happens-before 的关系,而不是线程二。
308 | 309 |还有一个额外的细节,即 release-stored 的值可以经过被任意数量的「获取并修改」和「比较并交换」操作修改,但仍会导致与读取最终结果的 acquire-load 操作建立 happens-before 关系。
310 | 311 |例如,想象一个具有以下总修改顺序的原子变量:
312 |1. 初始化为 0
313 | 314 |2. Release-store 7
315 | 316 |3. Relaxed-fetch-and-add 1,改变 7 到 8
317 | 318 |4. Relaxed-fetch-and-add 1,改变 8 到 9
319 | 320 |5. Release-store 7
321 | 322 |6. Relaxed-swap 10,改变 7 到 10
323 | 324 |现在,如果我们在这个变量上执行 acquire-load 到 9,我们不仅与第四个操作(存储此值)建立了一个 happens-before 关系,同时也与第二个操作(存储 7)建立了该关系,即使第三个操作使用了 Relaxed 内存排序。
325 | 326 |相似地,如果我们在这个变量上执行 acquire-load 到 10,而该值是由一个 relaxed 操作写入的,我们仍然建立了与第五个操作(存储 7)的 happens-before 关系。因为它只是一个普通的 store 操作(不是「获取并修改」或「比较并交换」操作),它打破了规则链:我们没有与其他操作建立 happens-before 关系。
327 |522 | a.store(1, Release);523 | 可以由 release 屏障和随后的 relaxed store 组成: 524 |
525 | fence(Release); 526 | a.store(1, Relaxed);527 |
531 | a.load(Acquire);532 | 可以由 relaxed load 和随后的 acquire 屏障组成: 533 |
534 | a.load(Relaxed); 535 | fence(Acquire);536 |
553 | fence(Release); 554 | A.store(1, Relaxed); 555 | B.store(2, Relaxed); 556 | C.store(3, Relaxed);557 |
561 | A.load(Relaxed); 562 | B.load(Relaxed); 563 | C.load(Relaxed); 564 | fence(Acquire);565 |
let p = PTR.load(Acquire); 578 | if p.is_null() { 579 | println!("no data"); 580 | } else { 581 | println!("data = {}", unsafe { *p }); 582 | }583 |
let p = PTR.load(Relaxed); 587 | if p.is_null() { 588 | println!("no data"); 589 | } else { 590 | fence(Acquire); 591 | println!("data = {}", unsafe {*p }); 592 | }593 |
除了常规的原子屏障,Rust 标准库还提供了编译器屏障:std::sync::atomic::compiler_fence
。它的签名与我们上面讨论的这些常规 fence()
不同,但它的效果仅限于编译器。与原子屏障不同,例如,它并不会阻止处理器重排指令。在绝大多数屏障的用例中,编译器屏障是不够的。
在实现 Unix 信号处理程序或嵌入式系统上的中断时,可能会出现的用例。这些机制可以突然中断一个线程,暂时在同一处理器内核上执行一个不相关的函数。由于它发生在同一处理器内核上,处理器可能影响内存排序的常规方式不适用。(更多细节请参考第七章)在这种情况下,编译器屏障可能阻隔,这样可以节省一条指令并且希望提高性能。
644 | 645 |另一用例涉及进程级内存屏障。这种技术超出了 Rust 内存模型的范畴,并且仅在某些操作系统上受支持:在 Linux 上通过 membarrier 系统调用,在 Windows 上使用 FlushProcessWriterBuffers 函数。它有效地允许一个线程强制向所有并发运行的线程注入(顺序一致性)原子屏障。这使得我们可以使用轻量级的编译器屏障和重型的进程级屏障替换两个匹配的屏障。如果轻量级屏障一侧的代码执行效率更高,这可以提高整体性能。(请参阅 crates.io
上的 membarrier crate 文档,了解更多详细信息和在 Rust 中使用这种屏障的跨平台方法。)
编译器屏障也可以是一个有趣的工具,用于探索处理器对内存排序的影响。
648 | 649 |在第七章“一个实验”中,我们将故意使用编译器屏障替换常规屏障。这将让我们在使用错误的内存排序时体验到处理器的微妙但潜在的灾难性影响。
650 |715 | 下一篇,第四章:构建我们自己的自旋锁 716 |
717 | 718 | [^1]:298 | 下一篇,第五章:构建我们自己的 Channel 299 |
300 | -------------------------------------------------------------------------------- /5_Building_Our_Own_Channels.md: -------------------------------------------------------------------------------- 1 | # 第五章:构建我们自己的 Channel 2 | 3 | (英文版本) 4 | 5 | *Channel* 可以被用于在线程之间发送数据,并且它有很多变体。一些 channel 仅能在一个发送者和一个接收者之间使用,而另一些可以在任意数量的线程之间发送,或者甚至允许多个接收者。一些 channel 是阻塞的,这意味着接收(有时也包括发送)是一个阻塞操作,这会使线程进入睡眠状态,直到你的操作完成。一些 channel 针对吞吐量进行优化,而另一些针对低延迟进行优化。 6 | 7 | 这些变体是无穷尽的,没有一种通用版本在所有场景都适合的。 8 | 9 | 在该章节,我们将实现一个相对简单的 channel,不仅可以探索更多的原子应用,同时也可以了解如何在 Rust 类型系统中捕获我们的需求和假设。 10 | 11 | ## 一个简单的以 mutex 为基础的 Channel 12 | 13 | (英文版本) 14 | 15 | 一个基础的 channel 实现并不需要任何关于原子的知识。我们可以接收 `VecDeque`,它根本上是一个 `Vec`,允许在两端高效地添加和移除元素,并使用 Mutex 保护它,以允许多个线程访问。然后,我们使用 `VecDeque` 作为已发送但尚未接受数据的消息队列。任何想要发送消息的线程只需要将其添加到队列的末尾,而任何想要接受消息的线程只需从队列的前端删除一个消息。 16 | 17 | 还有一点需要补充,用于将接收操作阻塞的 Condvar(参见[第一章“条件变量”](./1_Basic_of_Rust_Concurrency.md#条件变量)),当有新的消息,它会通知正在等待的接收者。 18 | 19 | 这样做的实现可能非常简短且相对直接,如下所示: 20 | 21 | ```rust 22 | pub struct Channel如果你对 channel 实现还不满意,这里有一个微妙的变体,可以节省一字节的内存。
296 | 297 |我们使用单个原子 AtomicU8
表示所有 4 个状态,而不是使用两个分开的布尔值去表示 channel 的状态。我们必须使用 compare_exchange
来原子地检查 channel 是否处于预期状态,并将其更改为另一个状态,而不是原子交换布尔值。
const EMPTY: u8 = 0; 300 | const WRITING: u8 = 1; 301 | const READY: u8 = 2; 302 | const READING: u8 = 3; 303 | 304 | pub struct Channel<T> { 305 | message: UnsafeCell<MaybeUninit<T>>, 306 | state: AtomicU8, 307 | } 308 | 309 | unsafe impl<T: Send> Sync for Channel<T> {} 310 | 311 | impl<T> Channel<T> { 312 | pub const fn new() -> Self { 313 | Self { 314 | message: UnsafeCell::new(MaybeUninit::uninit()), 315 | state: AtomicU8::new(EMPTY), 316 | } 317 | } 318 | 319 | pub fn send(&self, message: T) { 320 | if self.state.compare_exchange( 321 | EMPTY, WRITING, Relaxed, Relaxed 322 | ).is_err() { 323 | panic!("can't send more than one message!"); 324 | } 325 | unsafe { (*self.message.get()).write(message) }; 326 | self.state.store(READY, Release); 327 | } 328 | 329 | pub fn is_ready(&self) -> bool { 330 | self.state.load(Relaxed) == READY 331 | } 332 | 333 | pub fn receive(&self) -> T { 334 | if self.state.compare_exchange( 335 | READY, READING, Acquire, Relaxed 336 | ).is_err() { 337 | panic!("no message available!"); 338 | } 339 | unsafe { (*self.message.get()).assume_init_read() } 340 | } 341 | } 342 | 343 | impl<T> Drop for Channel<T> { 344 | fn drop(&mut self) { 345 | if *self.state.get_mut() == READY { 346 | unsafe { self.message.get_mut().assume_init_drop() } 347 | } 348 | } 349 | }350 | 351 |
756 | 下一篇,第六章:构建我们自己的“Arc” 757 |
758 | -------------------------------------------------------------------------------- /6_Building_Our_Own_Arc.md: -------------------------------------------------------------------------------- 1 | # 第六章:构建我们自己的“Arc” 2 | 3 | (英文版本) 4 | 5 | 6 | 在[第一章“引用计数”](./1_Basic_of_Rust_Concurrency.md#引用计数)中,我们了解了 `std::sync::Arc使用 Miri 运行测试是非常有用的。Miri 是一个实验性,但非常有用并且强力的工具,用于检查各种未定义形式的不安全代码。
199 | 200 |Miri 是 Rust 编译器中间级别中间表示的解释器。这意味着它不会将你的代码编译成本机处理器指令,而是通过当像类型和生命周期是仍然可用时进行解释。因此,Miri 运行程序的速度比正常运行速度慢很多,但能够检测许多导致未定义行为的错误。
201 | 202 |它包括检测数据竞争的实验性支持,这允许它检测内存排序问题。
203 | 204 |有关更多使用 Miri 的细节和指导,请参见它的 GitHub 页面
205 |