Send 与 Sync trait
本教程共 78 篇 · 第 69 篇 · 更新于 2026-08-08 · 约 7 分钟阅读
本节目标:理解 Send 和 Sync 这两个”标记 trait”如何成为 Rust 并发安全的基石——Send 管所有权能否跨线程传递,Sync 管引用能否跨线程共享,并知道哪些常见类型没实现它们。
前面几章我们反复遇到编译器报错,比如”Rc 不能在线程间安全传递”。这些报错背后,站着两个不起眼但极其关键的 trait:Send 和 Sync。它们不定义任何方法,只是做标记,却决定了哪些类型能进线程、哪些不能。这一章把它们讲透。
1-1 两个标记 trait 的含义
Send 和 Sync 都是标记 trait(marker trait),本身没有任何方法要实现,纯粹用来给类型”贴标签”。
- 实现了
Send的类型,它的所有权可以安全地在线程之间传递。也就是说,你可以把这样一个值move到另一个线程去。 - 实现了
Sync的类型,它的引用(也就是&T)可以安全地在线程之间共享。也就是说,多个线程可以同时拿着指向同一份数据的引用。
这两者有个隐含关系:如果 T 的引用 &T 是 Send,那么 T 就是 Sync。道理也直观——要让多个线程共享 T,前提是你得能把 &T 这个引用送到别的线程去,而引用能送过去,正是 &T: Send。
Note绝大多数标准库类型都同时实现了
Send和Sync,包括i32、String、Vec这些。你平时几乎感觉不到它们的存在,直到你用了不该跨线程的东西,编译器才会拿它们说事。
1-2 从源码看 Rc 与 Arc 的区别
为什么 Rc 进不了线程,而 Arc 可以?看一眼标准库源码片段就清楚了:
// Rc 主动移除了 Send 和 Sync 的实现
impl<T: ?Sized> !marker::Send for Rc<T> {}
impl<T: ?Sized> !marker::Sync for Rc<T> {}
// Arc 则相反,实现了 Send 和 Sync(要求内部 T 也是 Send + Sync)
unsafe impl<T: ?Sized + Sync + Send> Send for Arc<T> {}
unsafe impl<T: ?Sized + Sync + Send> Sync for Arc<T> {}
Rc 用 ! 显式地”撤掉”了 Send 和 Sync 的实现,所以编译器一看到你把 Rc 往线程里塞,立刻报错。Arc 用 unsafe impl 声明自己实现了这俩 trait。差别的根源,上一章说过:Rc 的计数不是原子的,跨线程会乱;Arc 的计数是原子的,安全。
再看 Mutex 的源码约束:
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}
注意这里 T 只需要 Send,不需要 Sync。因为 Mutex 通过锁保证同一时刻只有一个线程能访问内部数据,它把”共享”变成了”串行访问”,所以即便里面的值本身不是 Sync,外面的 Mutex 也能是 Sync。这正是第 67 章”Arc + Mutex”组合能成立的底层原因。
1-3 哪些类型没实现 Send / Sync
绝大多数类型都有,但有几个常见的例外,必须记牢:
- 裸指针(
*const T、*mut T):既没实现Send也没实现Sync,因为它本身没有任何安全保证。 UnsafeCell及其衍生类型:Cell和RefCell不是Sync(实际上也不是Send用于共享),因为它们允许在不可变引用下修改数据,多线程下会出乱子。所以前面章节强调,异步多线程里不能用RefCell。Rc:两个都没实现,计数不是线程安全的。
如果你自己定义了一个复合类型(比如结构体),只要它的每个字段都实现了 Send/Sync,那整个结构体自动就实现了;反过来,只要有一个字段没实现,整个类型就不是 Send/Sync。这个”自动派生”规则让绝大多数自定义类型天然安全。
1-4 手动实现 Send / Sync 要 unsafe
自动派生覆盖不了所有情况。比如你想让裸指针也能被某个自定义类型安全地跨线程传递,就得手动实现。但因为这是 unsafe trait,必须写在 unsafe 块里,并且由你自己担保并发安全性:
use std::thread;
#[derive(Debug)]
struct MyBox(*mut u8);
unsafe impl Send for MyBox {}
fn main() {
let p = MyBox(5 as *mut u8);
let t = thread::spawn(move || {
println!("{:?}", p);
});
t.join().unwrap();
}
这里用了一个 newtype 技巧:包一层 struct MyBox(*mut u8)。因为裸指针本身不能直接 impl,但你可以给”包着它的结构体”实现 Send。同样地,想让它能被共享引用跨线程,就再加上 unsafe impl Sync for MyBox {}。
Warning手动实现
Send/Sync是把安全责任完全压在你肩上。编译器不再帮你检查,写错了就是未定义行为。除非你非常清楚自己在干嘛,否则不要手动实现这两个 trait。绝大多数项目一辈子都不需要碰它们。
1-5 为什么这套设计很厉害
Send 和 Sync 最妙的地方在于:它们把”并发安全”这件事,从运行时检查搬到了编译期类型系统。一个值能不能跨线程,编译器在编译时就通过 trait 约束拦住了。你不需要像在 C++ 里那样,靠小心翼翼地读代码、跑测试去防数据竞争——编译器替你守门。
这也是 Rust 敢说”无畏并发”(fearless concurrency)的底气所在。它不保证你写不出并发 bug(死锁、逻辑错误照样能写出来),但它保证不会出现”数据竞争”这种最阴险、最难调试的内存安全问题。
1-6 如何判断一个类型是否 Send/Sync
实际写代码时你很少手动实现这两个 trait,但经常需要判断”我这个类型能不能进线程”。判断规则很机械:先看它有没有裸指针、Rc、RefCell、UnsafeCell 这类明确不实现的成员;如果没有,再看它的每个字段——只要所有字段都实现了 Send/Sync,整个复合类型就自动实现。所以你定义一个普通结构体,里面全是 String、Vec、i32 这些,它天然就是 Send + Sync,能直接 move 进线程或用 Arc 共享。
反过来,如果你的结构体里包了一个 Rc,那它就不是 Send,thread::spawn 会直接报错。这时候把 Rc 换成 Arc 通常就解决了。编译器报”cannot be sent between threads safely”时,顺着报错改类型,基本就是这条路径:把非线程安全的成员换成线程安全的版本。
Tip记不住例外名单?最常用的就三个:
Rc(计数非原子)、RefCell/Cell(内部可变性靠非线程安全机制)、裸指针(无任何保证)。避开它们,你的类型基本都是Send + Sync。
1-7 小结
Send 和 Sync 是 Rust 并发安全的规则基石:Send 决定所有权能否跨线程搬,Sync 决定引用能否跨线程共享。几乎一切类型都自动实现它们,裸指针、Rc、RefCell 是典型例外。需要手动实现时要用 unsafe 并自己担保安全。理解了这俩 trait,你就掌握了对”什么能进线程”的终极解释权。下一章我们把整段并发内容做一次总结。