8.2 Send 与 Sync

Send/Sync 的含义与实现

译文 · 基于 The Rustonomicon

Send 与 Sync

原文链接: https://doc.rust-lang.org/nomicon/send-and-sync.html

  并非一切都服从 inherited mutability。有些类型允许在 mutate 内存位置的同时拥有多个别名。除非这些类型用同步管理访问,它们绝对不安全。Rust 通过 Send 和 Sync trait 捕获这一点。

  • 若类型可以安全地发送到另一线程,则它是 Send。
  • 若类型可以在线程间安全共享,则它是 Sync(T 是 Sync 当且仅当 &T 是 Send)。

  Send 和 Sync 是 Rust 并发故事的基础。因此存在大量特殊工具使其正确工作。首先,它们是 [unsafe trait]。这意味着实现它们是不安全的,其他 unsafe 代码可以假设它们被正确实现。由于它们是标记 trait(没有 methods 等关联项),正确实现即意味着具有实现者应有的内在性质。错误实现 Send 或 Sync 会导致未定义行为。

  Send 和 Sync 也是自动 derived trait。这意味着与其他 trait 不同,若类型完全由 Send 或 Sync 类型组成,则它是 Send 或 Sync。几乎所有 primitive 都是 Send 和 Sync,因此你接触的几乎所有类型都是 Send 和 Sync。

  主要例外包括:

  • raw pointer 既不是 Send 也不是 Sync(因为没有安全护栏)。
  • UnsafeCell 不是 Sync(因此 Cell 和 RefCell 也不是)。
  • Rc 不是 Send 或 Sync(因为 refcount 是共享且未同步的)。

  Rc 和 UnsafeCell 从根本上不是线程安全的:它们 enable 未同步的共享可变状态。然而 raw pointer 严格来说被标为 thread-unsafe 更像是一种* lint*。对 raw pointer 做任何有用的事都需要解引用,而解引用本身已是 unsafe。从这个意义上,可以认为标为 thread-safe 也「没问题」。

  然而它们不是 thread-safe 很重要,以防止包含它们的类型被自动标为 thread-safe。这些类型有复杂的未跟踪所有权,作者未必认真考虑过 thread safety。对 Rc,我们有一个好例子:包含 definitely 不是 thread-safe 的 *mut。

  不能自动 derived 的类型可以按需实现:

1
2
3
4
struct MyBox(*mut u8);

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

  在极其罕见的情况下,类型被不当地自动 derived 为 Send 或 Sync,也可以 unimplement Send 和 Sync:

1
2
3
4
5
6
7
#![feature(negative_impls)]

// 我为某种同步原语有一些 magic 语义!
struct SpecialThreadToken(u8);

impl !Send for SpecialThreadToken {}
impl !Sync for SpecialThreadToken {}

  注意就其本身而言不可能错误 derive Send 和 Sync。只有被其他 unsafe 代码赋予特殊含义的类型,才可能因错误 Send 或 Sync 而惹麻烦。

  大多数 raw pointer 用法应封装在足够抽象之后,以便 derive Send 和 Sync。例如 Rust 所有标准集合尽管 pervasive 使用 raw pointer 管理分配和复杂所有权,在包含 Send 和 Sync 类型时仍是 Send 和 Sync。类似地,这些集合的大多数 iterator 是 Send 和 Sync,因为它们 largely 表现得像对集合的 & 或 &mut。

示例

  Box 因各种原因由编译器实现为特殊 intrinsic 类型,但我们可以自己实现类似行为,以看何时 sound 地实现 Send 和 Sync。叫它 Carton。

  先写把栈上分配的值转移到堆上的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# pub mod libc {
#    pub use ::std::os::raw::{c_int, c_void};
#    #[allow(non_camel_case_types)]
#    pub type size_t = usize;
#    unsafe extern "C" { pub fn posix_memalign(memptr: *mut *mut c_void, align: size_t, size: size_t) -> c_int; }
# }
use std::{
    mem::{align_of, size_of},
    ptr,
    cmp::max,
};

struct Carton<T>(ptr::NonNull<T>);

impl<T> Carton<T> {
    pub fn new(value: T) -> Self {
        // 在堆上分配足够存储一个 T 的内存
        assert_ne!(size_of::<T>(), 0, "零大小类型超出本例范围");
        let mut memptr: *mut T = ptr::null_mut();
        unsafe {
            let ret = libc::posix_memalign(
                (&mut memptr as *mut *mut T).cast(),
                max(align_of::<T>(), size_of::<usize>()),
                size_of::<T>()
            );
            assert_eq!(ret, 0, "分配失败或对齐无效");
        };

        // NonNull 只是确保指针非 null 的包装
        let ptr = {
            // Safety: memptr 可解引用,因为我们从引用创建且拥有独占访问
            ptr::NonNull::new(memptr)
                .expect("若 posix_memalign 返回 0 则保证非 null")
        };

        // 把值从栈移到堆上分配的位置
        unsafe {
            // Safety: 若非 null,posix_memalign 给出可写且正确对齐的 ptr
            ptr.as_ptr().write(value);
        }

        Self(ptr)
    }
}

  这不太有用,用户给出值后无法访问。Box 实现 Deref 和 DerefMut 以便访问内部值。我们也来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use std::ops::{Deref, DerefMut};

# struct Carton<T>(std::ptr::NonNull<T>);
#
impl<T> Deref for Carton<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        unsafe {
            // Safety: 指针已对齐、已初始化且可解引用,
            //   由 [`Self::new`] 的逻辑保证。我们要求读者借用 Carton,
            //   返回值的 lifetime 被 elide 到输入的 lifetime。借用检查器会
            //   强制在返回的引用 drop 之前无人能 mutate Carton 内容
            self.0.as_ref()
        }
    }
}

impl<T> DerefMut for Carton<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        unsafe {
            // Safety: 指针已对齐、已初始化且可解引用,
            //   由 [`Self::new`] 的逻辑保证。我们要求写者 mutably 借用 Carton,
            //   返回值的 lifetime 被 elide 到输入的 lifetime。借用检查器会
            //   强制在返回的可变引用 drop 之前无人能访问 Carton 内容
            self.0.as_mut()
        }
    }
}

  最后,思考 Carton 是否 Send 和 Sync。某物可以安全 Send,除非它在不强制独占访问的情况下与其他东西共享可变状态。每个 Carton 有唯一指针,我们没问题。

1
2
3
4
# struct Carton<T>(std::ptr::NonNull<T>);
// Safety: 除我们外无人持有 raw pointer,若 T 可安全转移,
// 我们可以安全地把 Carton 转移到另一线程
unsafe impl<T> Send for Carton<T> where T: Send {}

  Sync 呢?Carton 要 Sync,必须强制不能在对 &Carton 中某物写入的同时,从另一 &Carton 读或写同一某物。由于需要 &mut Carton 才能写指针,且借用检查器强制可变引用必须独占,sound 地使 Carton sync 没有问题。

1
2
3
4
5
6
7
# struct Carton<T>(std::ptr::NonNull<T>);
// Safety: 由于存在从 `&Carton<T>` 到 `&T` 的公开、未同步路径(如 `Deref`),
// 若 `T` 不是 Sync,则 `Carton<T>` 不能是 `Sync`。
// 反之,`Carton` 本身完全不使用内部可变性:
// 所有 mutation 都通过独占引用(`&mut`)进行。因此只要 `T` 是 `Sync`,
// `Carton<T>` 就是 `Sync` 即可:
unsafe impl<T> Sync for Carton<T> where T: Sync  {}

  断言类型 Send 和 Sync 时,通常需要强制每个 contained 类型是 Send 和 Sync。写 behave 像标准库类型的自定义类型时,可以断言有相同要求。例如以下代码断言 Carton 在同类 Box 是 Send 时 Send,此例即 T 是 Send。

1
2
# struct Carton<T>(std::ptr::NonNull<T>);
unsafe impl<T> Send for Carton<T> where Box<T>: Send {}

  目前 Carton<T> 有内存泄漏,从不释放分配的内存。修复后 Send 有新要求:需知 free 可在另一线程分配产生的指针上调用。可在 libc::free 文档中确认。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# struct Carton<T>(std::ptr::NonNull<T>);
# mod libc {
#     pub use ::std::os::raw::c_void;
#     unsafe extern "C" { pub fn free(p: *mut c_void); }
# }
impl<T> Drop for Carton<T> {
    fn drop(&mut self) {
        unsafe {
            libc::free(self.0.as_ptr().cast());
        }
    }
}

  一个 nice 的反例是 MutexGuard:注意它不是 Send。MutexGuard 的实现使用库,要求确保不尝试释放你在不同线程获得的锁。若能把 MutexGuard Send 到另一线程,析构函数会在你发送到的线程运行,违反该要求。MutexGuard 仍可以是 Sync,因为发送到另一线程的只能是 &MutexGuard,而 drop 引用什么都不做。

  TODO: 更好地解释什么能或不能 Send 或 Sync。仅诉诸数据竞争是否足够?

最后修改 August 11, 2026: 更新 (70a5af133)