通过 Sync
和 Send
Trait 实现可扩展的并发
有趣的是,Rust 语言的并发特性 *非常* 少。我们在本章到目前为止讨论的几乎所有并发特性都是标准库的一部分,而不是语言本身。 你处理并发的方式不限于语言或标准库;你可以编写自己的并发特性或使用其他人编写的特性。
但是,有两个并发概念嵌入到了语言中:std::marker
trait Sync
和 Send
。
使用 Send
允许在线程之间转移所有权
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>>
。 当我们切换到 Arc<T>
(它是 Send
)时,代码编译通过了。
任何完全由 Send
类型组成的类型也会自动标记为 Send
。 几乎所有的基本类型都是 Send
,除了原始指针,我们将在第 19 章中讨论。
使用 Sync
允许从多个线程访问
Sync
标记 trait 表示实现 Sync
的类型可以安全地从多个线程引用。 换句话说,如果 &T
(对 T
的不可变引用)是 Send
,则任何类型 T
都是 Sync
,这意味着该引用可以安全地发送到另一个线程。 与 Send
类似,基本类型是 Sync
,完全由 Sync
类型组成的类型也是 Sync
。
智能指针 Rc<T>
也不是 Sync
,原因与它不是 Send
的原因相同。 RefCell<T>
类型(我们在第 15 章中讨论过)以及相关的 Cell<T>
类型系列都不是 Sync
。 RefCell<T>
在运行时进行的借用检查的实现不是线程安全的。 智能指针 Mutex<T>
是 Sync
,可以用来与多个线程共享访问,如你在 “在多个线程之间共享 Mutex<T>
”部分所见。
手动实现 Send
和 Sync
是不安全的
由于由 Send
和 Sync
trait 组成的类型也会自动是 Send
和 Sync
,因此我们不必手动实现这些 trait。 作为标记 trait,它们甚至没有任何方法需要实现。 它们只是用于强制执行与并发相关的约束。
手动实现这些 trait 涉及到实现不安全的 Rust 代码。 我们将在第 19 章讨论使用不安全的 Rust 代码; 现在,重要信息是构建不是由 Send
和 Sync
部分组成的新并发类型需要仔细考虑以维护安全保证。 “The Rustonomicon” 有关这些保证以及如何维护它们的更多信息。
总结
这并不是你在本书中最后一次看到并发:第 20 章中的项目将在比这里讨论的较小示例更实际的情况下使用本章中的概念。
如前所述,由于 Rust 处理并发的方式很少是语言的一部分,因此许多并发解决方案都是作为 crate 实现的。 这些比标准库的更新速度更快,因此请务必在线搜索当前最先进的 crate,以便在多线程情况下使用。
Rust 标准库提供了用于消息传递的通道和智能指针类型,例如 Mutex<T>
和 Arc<T>
,它们在并发上下文中可以安全使用。 类型系统和借用检查器可确保使用这些解决方案的代码不会以数据竞争或无效引用的方式结束。 一旦你使代码编译,你就可以放心,它会在多个线程上愉快地运行,而不会出现其他语言中常见的难以追踪的错误。 并发编程不再是一个需要害怕的概念:大胆前进,让你的程序并发,无所畏惧!
接下来,我们将讨论如何以惯用的方式建模问题并构建解决方案,随着你的 Rust 程序越来越大。 此外,我们将讨论 Rust 的惯用法与你可能熟悉的面向对象编程中的惯用法之间的关系。