使用 SyncSend Traits 的可扩展并发

有趣的是,Rust 语言的并发特性非常少。到目前为止,我们在本章中讨论的几乎所有并发特性都是标准库的一部分,而不是语言的一部分。您处理并发的选项不限于语言或标准库;您可以编写自己的并发特性或使用其他人编写的特性。

但是,有两个并发概念嵌入在语言中:std::marker traits SyncSend

使用 Send 允许在线程之间转移所有权

Send 标记 trait 指示可以线程之间转移实现了 Send 类型的value的所有权。几乎所有 Rust 类型都是 Send,但也有一些例外,包括 Rc<T>:这不能是 Send,因为如果您克隆了一个 Rc<T> 值并尝试将克隆的所有权转移到另一个线程,则两个线程可能会同时更新引用计数。因此,Rc<T> 被实现用于您不想支付线程安全性能损失的单线程情况。

因此,Rust 的类型系统和 trait bounds 确保您永远不会意外地跨线程不安全地发送 Rc<T> 值。当我们在 Listing 16-14 中尝试这样做时,我们得到了错误 the trait Send is not implemented for Rc<Mutex<i32>>。当我们切换到 Arc<T>(它是 Send)时,代码编译通过了。

完全由 Send 类型组成的任何类型也会自动标记为 Send。除了我们将在第 20 章中讨论的原始指针之外,几乎所有原始类型都是 Send

使用 Sync 允许从多个线程访问

Sync 标记 trait 指示从多个线程引用实现 Sync 的类型是安全的。换句话说,如果 &T(对 T 的不可变引用)是 Send,则任何类型 T 都是 Sync,这意味着引用可以安全地发送到另一个线程。与 Send 类似,原始类型是 Sync,并且完全由 Sync 类型组成的类型也是 Sync

Sync 是 Rust 中与“线程安全”这个口语化短语最相似的概念,即特定数据可以被多个并发线程安全地使用。具有单独的 SendSync traits 的原因是,一种类型有时可以是其中之一,或两者兼有,或两者都不是。例如

  • 智能指针 Rc<T> 也既不是 Send 也不是 Sync,原因如上所述。
  • RefCell<T> 类型(我们在第 15 章中讨论过)和相关的 Cell<T> 类型系列是 Send(如果 T: Send),但它们不是 SyncRefCell 可以跨线程边界发送,但不能并发访问,因为 RefCell<T> 在运行时执行的借用检查的实现不是线程安全的。
  • 智能指针 Mutex<T>SendSync,可以用于与多个线程共享访问,正如您在“在多个线程之间共享 Mutex<T>中看到的那样节。
  • Mutex::lock 返回的类型 MutexGuard<'a, T>Sync(如果 T: Sync)但不是 Send。它专门不是 Send,因为某些平台要求互斥锁必须由锁定它们的同一线程解锁

手动实现 SendSync 是不安全的

因为由 SendSync traits 组成的类型也会自动成为 SendSync,所以我们不必手动实现这些 traits。作为标记 traits,它们甚至没有任何方法需要实现。它们仅用于强制执行与并发相关的不变性。

手动实现这些 traits 涉及实现 unsafe Rust 代码。我们将在第 20 章中讨论使用 unsafe Rust 代码;现在,重要的信息是,构建不是由 SendSync 部分组成的新并发类型需要仔细考虑以维护安全保证。“The Rustonomicon” 提供了有关这些保证以及如何维护它们的更多信息。

总结

这不是您在本书中最后一次看到并发:下一整章都侧重于异步编程,第 21 章中的项目将在比此处讨论的较小示例更实际的情况下使用本章中的概念。

如前所述,由于 Rust 处理并发的方式很少是语言的一部分,因此许多并发解决方案都以 crates 的形式实现。这些方案的演变速度比标准库快,因此请务必在线搜索当前最先进的 crates,以便在多线程情况下使用。

Rust 标准库为消息传递和智能指针类型(例如 Mutex<T>Arc<T>)提供了通道,这些类型在并发上下文中可以安全使用。类型系统和借用检查器确保使用这些解决方案的代码不会以数据竞争或无效引用结束。一旦您的代码编译通过,您就可以放心,它将在多个线程上愉快地运行,而不会出现其他语言中常见的难以追踪的错误。并发编程不再是一个需要害怕的概念:前进,无畏地使您的程序并发吧!