Rust Send和Sync

不是所有类型都遵守可变性的原则。有一些类型允许你拥有一块内存的多个别名,同时还改变内存的值。除非这些类型使用同步来控制访问,否则它们就不是线程安全的。Rust根据SendSync这两个trait获取相关信息。

  • 如果一个类型可以安全地传递给另一个线程,这个类型是Send
  • 如果一个类型可以安全地被多个线程共享(也就是&TSend),这个类型是Sync

SendSync是Rust并发机制的基础。因此,Rust赋予它们许多的特性,以保证它们能正确工作。首当其冲的,它们都是非安全trait。这表明它们的实现也是非安全的,而其他的非安全代码则可以假设这些实现是正确的。由于它们是标志trait(它们没有任何关联的方法),“正确地实现”仅仅意味着实现满足它所需要的内部特征。不正确地实现SendSync会导致未定义行为。

SendSync还是自动推导的trait。和其他的trait不同,如果一个类型完全由SendSync组成,那么这个类型本身也是SendSync。几乎所有的基本类型都是SendSync,因此你能见到的很多类型也就都是SendSync

主要的例外情况有:

  • 裸指针不是Send也不是Sync(因为它们没有安全性保证)
  • UnsafeCell不是Sync(所以CellRefCell也不是)
  • Rc不是SendSync(因为引用计数是共享且非同步的)

RcUnsafeCell是典型的非线程安全的:它们允许非同步地共享可变状态。可是,裸指针严格来说并不一定非得是非线程安全不可。通过裸指针做任何有意义的事情都需要先对它解引用,这一步就已经是非安全的了。从这个角度来说,有人可能会认为把它标为线程安全的也未尝不可。

可是,它们被标为非线程安全的主要目的是避免包含它们的类型自动成为线程安全的。这些类型都有着重要的不可追踪的所有权,保证它们线程安全需要花费大量的精力,而他们的作者不太可能做到这一点。Rc就是一个很好的例子,一个包含*mut的类型绝对不能是线程安全的。

不是自动推导的类型也可以很容易地实现SendSync

struct MyBox(*mut u8);

unsafe impl Send for MyBox {}
unsafe impl Sync for MyBox {}

还有一个很少见的场景,一个类型被自动推导为SendSync,但是它其实不满足二者的要求。这是我们可以去掉SendSync的实现:

#![feature(option_builtin_traits)]

// 我对于同步的基础类型有着神奇的语义
struct SpecialThreadToken(u8);

impl !send for SpecialThreadToken {}
impl !Sync for SpecialThreadToken {}

注意,一个类型自己不可能被不正确地推导为SendSync。只有当类型和其他的非安全代码一起实现了一些特殊行为时,它才可能成为一个不正确的SendSync

大部分使用裸指针的类型都应该把裸指针用一种抽象隐藏起来,以保证类型可以被推导为SendSync。比如,所有Rust的标准集合类型都是SendSync(在他们包含SendSync类型的情况下),虽然它们都大量使用了裸指针处理内存分配和复杂的所有权。类似的,大部分这些集合的迭代器也是SendSync,因为它们的行为很像这些集合的&或者&mut

Rust几乎全部抄袭了C11关于原子操作的内存模型。这么做并不是因为这个模型多么的优秀或者易于理解。事实上,这个模型非常的复杂,而且有一些已知的缺陷。不过,所有的原子操作模型其实都不怎么样,我们不得不因此做出一些妥协。至 ...