基本概述

目前为止,我们一直假定所有值都可以在进程间自由转移和共享。大多数情况下确实如此,但 Rust 代码的彻底安全取决于两个内置特型: std::marker::Sendstd::marker::Sync

  • 实现 Send 的类型可以安全地把值传到另一个线程,即它们可以在线程间转移
  • 实现 Sync 的类型可以安全地把不可修改引用传到另一个线程,即它们可以在线程间共享

这里所谓的安全,就是我们一直反复强调的意思:没有数据争用其他未定义行为

线程安全: Send 与 Sync - 图1

为何 Rc、RefCell 和 裸指针不可以在多线程间使用?如何让裸指针可以在多线程使用?我们一起来探寻下这些问题的答案

无法用于多线程的 Rc

先来看一段多线程使用 Rc 的代码

线程安全: Send 与 Sync - 图2

以上代码将 v 的所有权通过 move 转移到子线程中,看似正确实则会报错

线程安全: Send 与 Sync - 图3

如果可以在线程间共享 Rc 会导致什么问题?如果两个线程恰好同时克隆这个 Rc ,就会出现数据争用,因为两个线程都会增加共享引用计数。结果引用计数可能会变得不准确,从而导致将来的 “释放后还使用” 或 双重释放,这都是未定义行为

线程安全: Send 与 Sync - 图4

Send 和 Sync

Send 和 Sync 是 Rust 安全并发的重中之重,但是实际上它们只是 标记特征(marker trait),该特征未定义任何行为,因此非常适合用于标记。来看看它们的作用

  • 实现 Send 的类型可以在线程间安全的传递其所有权
  • 实现 Sync 的类型可以在线程间安全的共享(通过引用)

这里还有一个潜在的依赖:一个类型要在线程间安全的共享的前提是,指向它的引用必须能在线程间传递。因为如果引用都不能被传递,我们就无法在多个线程间使用引用取访问同一个数据了。由此可知,若类型 T 的引用 &T 是 Send,则 T 是 Sync

std::marker::PhantomData

选择不使用 Send/Sync 的方法是给你的类型添加一个不实现该特征的字段。为此,特殊的 std::marker::PhantomData 类型通常会派上用场。该类型被编译器视为 T ,但它在运行时实际上并不存在。它是零大小的类型,不占用空间

线程安全: Send 与 Sync - 图5

在此示例中,如果 handle 是它的唯一字段,则 X 将同时是 Send 和 Sync。但是,我们添加了一个零大小的 PhantomData> 字段,它被视为 Cell<()> 。由于 Cell<()> 不是 Sync所以 X 也不是。然而,它仍然是 Send,因为它的所有字段都实现了 Send

未实现 Send 和 Sync 的类型

在 Rust 中,几乎所有类型都默认实现了 Send 和 Sync,而且由于这两个特征都是可自动派生的特征(通过 Derive 派生),意味着一个复合类型(例如结构体),只要它内部的所有成员都实现了 Send 或者 Sync,那么它就自动实现了 Send 或 Sync。正是因为以上规则,Rust 中绝大多数类型都实现了 Send 和 Sync,除了以下几个(事实上不止这几个,只不过它们比较常见)

  • 裸指针两者都没实现,因为它本身就没有任何安全保证
  • UnsafeCell 不是 Sync ,因此 Cell 和 RefCell 也不是
  • Rc 两者都没实现(因为内部的引用计数器不是线程安全的)

当然,如果是自定义的复合类型,那没实现那哥俩的就较为常见了:只要复合类型中有一个成员不是 Send 和 Sync,那么该复合类型也就不是 Send 或 Sync。手动实现 Send 和 Sync 是不安全的,通常并不需要手动实现 Send 和 Sync trait,实现者需要使用 unsafe 小心维护并发安全保证

为裸指针实现 Send

上面提到裸指针既没实现 Send,意味着下面代码会报错

线程安全: Send 与 Sync - 图6

报错跟之前无二: *mut u8 cannot be sent between threads safely ,但是有一个问题,我们无法为其直接实现 Send 特征,好在可以用 newtype 类型: struct MyBox(*mut u8); 。还记得之前的规则吗:复合类型中有一个成员没实现 Send,该复合类型就不是 Send,因此我们需要手动为它实现

线程安全: Send 与 Sync - 图7

此时,我们的指针已经可以欢快的在多线程间撒欢,以上代码很简单,但有一点需要注意:Send 和 Sync 是 unsafe 特征,实现时需要用 unsafe 代码块包裹

为裸指针实现 Sync

由于 Sync 是多线程间共享一个值,大家可能会想这么实现

线程安全: Send 与 Sync - 图8

关于这种用法,在多线程章节也提到过,线程如果直接去借用其它线程的变量,会报错:closure may outlive the current function,原因在于编译器无法确定主线程 main 和子线程 t 谁的生命周期更长,特别是当两个线程都是子线程时,没有任何人知道哪个子线程会先结束,包括编译器。因此我们得配合 Arc 去使用

线程安全: Send 与 Sync - 图9

上面代码将智能指针 v 的所有权转移给新线程,同时 v 包含了一个引用类型 b,当在新的线程中试图获取内部的引用时,会报错

线程安全: Send 与 Sync - 图10

因为我们访问的引用实际上还是对主线程中的数据的借用,转移进来的仅仅是外层的智能指针引用。要解决很简单,为 MyBox 实现 Sync

线程安全: Send 与 Sync - 图11