使用 Send 和 Sync 实现可扩展并发
有趣的是,本章到目前为止我们讨论的几乎每一个并发 特性都是标准库的一部分,而不是语言本身。你处理 并发的选择并不局限于语言或标准库;你可以编写 自己的并发特性,或使用他人编写的并发特性。
然而,在嵌入语言本身(而非标准库)的关键并发
概念中,就有 std::marker 中的 Send 和 Sync trait。
在线程之间转移所有权
Send 标记 trait 表明实现了 Send 的类型的值的所有权
可以在线程之间转移。几乎所有 Rust 类型都实现了
Send,但有一些例外,包括 Rc<T>:它不能
实现 Send,因为如果你克隆了一个 Rc<T> 值并试图
将克隆的所有权转移到另一个线程,两个线程就可能同时
更新引用计数。因此,Rc<T> 被实现为用于
单线程场景,在这种场景下你不需要付出
线程安全的性能代价。
因此,Rust 的类型系统和 trait 约束确保你永远不会
意外地跨线程不安全地发送 Rc<T> 值。当我们在示例
16-14 中尝试这样做时,得到了错误 the trait `Send` is not implemented for `Rc<Mutex<i32>>`。当我们改用实现了
Send 的 Arc<T> 时,代码就编译通过了。
任何完全由 Send 类型组成的类型也会自动被标记为
Send。几乎所有原始类型都是 Send,除了原始指针,我们
将在第 20 章讨论它们。
从多个线程访问
Sync 标记 trait 表明实现了 Sync 的类型
可以安全地从多个线程中被引用。换句话说,任何类型 T
在 &T(T 的不可变引用)实现了 Send 时就实现了 Sync,
这意味着该引用可以被安全地发送到另一个线程。与 Send 类似,
原始类型都实现了 Sync,完全由实现了
Sync 的类型组成的类型也实现了 Sync。
智能指针 Rc<T> 出于与其未实现 Send 相同的原因
也未实现 Sync。RefCell<T> 类型(我们在第 15 章讨论过)
以及相关的 Cell<T> 类型家族都未实现
Sync。RefCell<T> 在运行时进行的借用检查实现
不是线程安全的。智能指针 Mutex<T> 实现了 Sync,可以
用来与多个线程共享访问,正如你在“对
Mutex<T> 的共享访问”一节中所看到的。
手动实现 Send 和 Sync 是不安全的
由于完全由其他实现了 Send 和
Sync trait 的类型组成的类型也会自动实现 Send 和
Sync,所以我们不必手动实现这些 trait。作为标记
trait,它们甚至没有任何方法需要实现。它们只是
用于强制实施与并发相关的不变量。
手动实现这些 trait 涉及实现不安全的 Rust 代码。
我们将在第 20 章讨论使用不安全的 Rust 代码;目前,重要的
信息是构建不由 Send 和
Sync 部分组成的新的并发类型需要谨慎思考,以维护
安全保证。《Rust 秘典》(The
Rustonomicon)提供了有关这些保证以及如何维护
它们的更多信息。
总结
这不是你在本书中最后一次看到并发:下一章 将专注于异步编程,第 21 章的项目将在比这里 讨论的小型示例更贴近实际的场景中运用本章的 概念。
如前所述,由于 Rust 处理并发的方式很少属于 语言本身,许多并发解决方案是以 crate 形式实现的。 它们比标准库演进得更快,所以请务必 在线搜索当前最先进的、可在多线程 场景中使用的 crate。
Rust 标准库提供了用于消息传递的通道,以及
诸如 Mutex<T> 和 Arc<T> 等可安全用于并发环境的智能
指针类型。类型系统和借用检查器确保
使用这些方案的代码不会以数据竞争或无效引用告终。
一旦你让代码编译通过,就可以放心它能在
多个线程上愉快地运行,而不会出现其他语言中常见的
难以追踪的 bug。并发编程不再是令人畏惧的
概念:放手去做,让你的程序无畏地并发吧!