首页
/ Comprehensive Rust 并发安全指南:深入理解 `Sync` 标记 trait 与线程安全语义

Comprehensive Rust 并发安全指南:深入理解 `Sync` 标记 trait 与线程安全语义

2026-09-09 12:45:59作者:裴锟轩Denise

本文基于 Google Android 团队的 Rust 课程项目 comprehensive-rust 中 send-sync 模块 的《Sync》章节展开。Sync 是 Rust 并发安全的两大基石之一,它定义了"值能否被多个线程同时安全共享"。读完本文,你将掌握 Sync 的精确定义与判据、它与 Send 的辩证关系、常见类型的四象限分类(Send + Sync / Send + !Sync / !Send + Sync / !Send + !Sync),以及 ArcMutex 等标准库类型背后的同步原语实现,从而在编写多线程代码时准确判断"什么能跨线程共享、什么不能"。

一、Sync 的定义:多线程同时访问的安全判据

Sync》章节 给出了课程中的权威定义:

类型 TSync 的,当且仅当从多个线程同时访问一个 T 的值是安全的。

这里的"访问"强调的是共享访问(shared access),即通过不可变引用 &T 并发读取。一个常见的直觉误区是把"线程安全"等同于"允许并发修改"——实际上 Sync 关心的是"并发读/并发使用共享引用"的安全性,而并发写入的互斥保障(如 Mutex)则是另一层机制(详见下文第四节)。

值得注意,Sync 并不要求类型是不可变的:Mutex<T> 本身是 Sync 的,但它保护的 T 却可以在锁内被可变访问。这正是 Rust 课程中反复强调的 interior mutability(内部可变性) 在并发领域的延伸。

二、SyncSend 的关系:T: Sync ⟺ &T: Send

课程原文给出了一个极其精炼的等价定义,这是理解整个并发类型系统的钥匙:

TSync 的,当且仅当 &TSend 的。

这句看似绕口的表述,其实是在说:一个类型如果适合"跨线程共享使用",那么它的引用也适合"跨线程传递"。课程在 <details> 折叠区中给出了推理链:

  • TSync,则它可以在多个线程之间共享,而不会出现数据竞争或其他同步问题;
  • 因此把 T 的引用移动到另一个线程也是安全的,因为该引用所指向的数据可以从任何线程安全地访问。

更直白地理解:Send 回答"这个值能不能搬到别的线程去",Sync 回答"这个值能不能被多个线程同时围观"。而"能被围观"的前提是"围观者(&T)能到达现场(跨线程传递)"。

两者在 marker-traits.md 中还有一组并行的对称定义:

  • SendT 可以安全地跨线程边界移动
  • Sync&T 可以安全地跨线程边界移动(即多个线程可以同时持有对同一数据的共享引用)。

课程将 SendSync 称为 marker traits(标记 trait)——它们像贴纸一样标记类型具有某种线程安全属性,可以在泛型约束中像普通 trait 一样使用(例如 fn foo<T: Send + Sync>()),但本身不提供任何方法。

三、Send / Sync 是 unsafe trait:编译器的自动推导与手动实现

marker-traits.md 明确指出:

  • SendSync 都是 unsafe traits
  • 编译器会为你的类型自动推导 Send / Sync,只要该类型的所有字段都是 Send / Sync 类型;
  • 当你确定某个类型在线程语义下是安全的、但编译器无法自动证明时,可以手动实现unsafe impl Send for MyType {})。

这意味着绝大多数"纯数据"类型(只有数字、布尔、字符串、Vec、其他自定义结构体组合)都会自动获得 Send + Sync,无需任何额外代码。而手动实现必须谨慎——一旦判断错误,就会把不安全的行为伪装成安全 API,属于典型的 unsafe 责任转移(在课程的 unsafe-rust 模块中有专门讲解)。

四、四象限分类:常见类型的 Send / Sync 组合

examples.md 以四象限的方式总结了 Rust 标准库常见类型的线程安全属性,这是判断"什么能跨线程"的实战速查表:

1. Send + Sync(绝大多数类型)

  • 标量与借用类型:i8f32boolchar&str&T 等;
  • 组合类型:(T1, T2) 元组、[T; N] 数组、&[T] 切片、struct { x: T }
  • 堆上容器:StringOption<T>Vec<T>Box<T>
  • 显式线程安全的同步原语:
    • Arc<T>:通过原子引用计数实现显式线程安全(详见第五节);
    • Mutex<T>:通过内部加锁实现显式线程安全(详见第六节);
    • mpsc::Sender<T>:自 Rust 1.72.0 起;
    • AtomicBoolAtomicU8 等原子类型:使用特殊的原子指令(atomic instructions)。

通用类型(generic types)通常满足:当类型参数是 Send + Sync 时,容器本身也是 Send + Sync

2. Send + !Sync(可搬走,但不可共享)

这类类型可以移动到其他线程,但本身不是线程安全的——通常因为存在内部可变性(interior mutability)

  • mpsc::Receiver<T>:接收端只能单线程消费;
  • Cell<T>:运行期借用检查,非线程安全;
  • RefCell<T>:运行期借用检查,非线程安全。

3. !Send + Sync(可共享,但不可搬走)

这类类型可以通过共享引用被多线程同时访问,但不能被移动到其他线程

  • MutexGuard<T>:它封装了 OS 级别的锁原语,必须在创建它的线程上释放(deallocate);但一个已经加锁的 mutex,其受保护的变量可以被任何共享到该 guard 的线程读取(除非 T 本身是 !Sync)。

4. !Send + !Sync(既不可搬走,也不可共享)

  • Rc<T>:每个 Rc<T> 都指向一个 RcBox<T>,其中包含非原子的引用计数——多线程并发增减计数会产生数据竞争;
  • *const T*mut T:Rust 假定裸指针可能有特殊的并发考量,因此默认不标记线程安全。

这四象限分类与 Arc 的文档注释相互印证:arc.md 中明确写出"Arc<T>Rc 的线程安全版本,使用原子操作";而 Rc 因为非原子计数落在 !Send + !Sync 象限。

五、源码佐证:ArcMutex 如何实现 Sync

课程的 shared-state 模块 提供了两个与 Sync 强相关的标准库类型实现细节:

Arc<T>:原子引用计数的共享所有权

arc.md 中的示例展示了 Arc 的典型使用——把同一个值克隆进 5 个线程,每个线程通过 Arc::clone 共享所有权,并通过 Drop 打印"由哪个线程释放":

use std::sync::Arc;
use std::thread;

struct WhereDropped(Vec<i32>);

impl Drop for WhereDropped {
    fn drop(&mut self) {
        println!("Dropped by {:?}", thread::current().id())
    }
}

fn main() {
    let v = Arc::new(WhereDropped(vec![10, 20, 30]));
    let mut handles = Vec::new();
    for i in 0..5 {
        let v = Arc::clone(&v);
        handles.push(thread::spawn(move || {
            std::thread::sleep(std::time::Duration::from_millis(500 - i * 100));
            let thread_id = thread::current().id();
            println!("{thread_id:?}: {v:?}");
        }));
    }
    drop(v);
    handles.into_iter().for_each(|h| h.join().unwrap());
}

课程要点:

  • Arc<T> 实现 Clone 无论 T 是否实现 Clone(克隆的是指针与计数,不是数据);
  • Arc<T> 实现 SendSync 当且仅当 T 同时实现 SendSync
  • Arc::clone() 有原子操作的开销,但克隆之后对 T 的使用是零成本的;
  • 注意引用环:Arc 不依赖垃圾回收来检测环,必要时可用 std::sync::Weak 打破循环引用。

Mutex<T>:锁内的可变访问与 Sync 的 blanket impl

mutex.md 强调 Mutex<T> 是"另一种形式的内部可变性"——通过只读接口 &Mutex<T> 提供对 T 的可变访问:

use std::sync::Mutex;

fn main() {
    let v = Mutex::new(vec![10, 20, 30]);
    println!("v: {:?}", v.lock().unwrap());

    {
        let mut guard = v.lock().unwrap();
        guard.push(40);
    }

    println!("v: {:?}", v.lock().unwrap());
}

课程给出的关键结论:

  • Rust 的 Mutex 看起来像一个"只含一个元素的集合"——受保护的数据被封装在锁内,不可能忘记加锁就去访问数据
  • 你可以从 &Mutex<T> 通过 lock() 拿到 &mut TMutexGuard 保证这个 &mut T 不会比锁的持有时间更长;
  • Mutex<T> 有一个 blanket 实现:impl<T: Send> Sync for Mutex<T>——只要受保护的数据 TSendMutex<T> 就是 Sync 的,即使 T 本身不是 Sync 也没关系(因为锁保证了独占访问);
  • Mutex<T> 同样满足"当且仅当 TSend 时才是 Send + Sync"的规则;
  • 读-写锁的对应物是 RwLock

Mutex::lock() 之所以返回 Result,是因为存在 poisoning(中毒)机制:持有锁的线程 panic 后,mutex 会进入中毒状态以标记受保护数据可能不一致,此时 lock() 会返回 PoisonError;你可以通过 into_inner() 强行取回数据继续使用。

六、为什么需要 Sync:fearless concurrency 与编译期防线

并发模块导言 阐述了 Sync / Send 存在的根本目的:

Rust 使用 OS 线程、互斥锁和通道提供完整的并发支持。类型系统在把许多并发 bug 变成编译期错误方面扮演着重要角色,这一思想被称为 fearless concurrency(无畏并发)——你可以信赖编译器来保证运行时的正确性。

Sync 就是这条防线上的核心哨兵:

  • 当你尝试把一个 !Sync 的类型(如 RefCell<T>)放进 thread::spawn 的闭包并让多个线程共享时,编译器会在编译期直接报错,而不是等到运行时出现数据竞争;
  • 泛型约束 T: Sync 是编写线程安全库的公共契约:例如 Arc<T>Send/Sync 实现、Mutex<T> 的 blanket impl,都通过 trait bound 向编译器声明自己的安全边界;
  • 课程还指出,单线程中帮助保证"并发访问"安全的工具(例如一个被调用的函数可能会修改参数、或保存参数引用供稍后读取)与多线程场景下的工具是同一套——共享与可变不共存(aliasing xor mutability)的原则贯穿始终。

七、教学脉络与进一步阅读

Sync 章节属于课程 Concurrency(并发) 部分的 send-sync 小节,其编排顺序为:先讲 marker traits 总览,再分别展开 SendSync 两个章节,最后用 Examples 的四象限总结收束。建议按以下路径继续深化:

  1. 先理解 marker traits 总览Send / Sync 的对称定义;
  2. 对照 Send 章节 理解"移动所有权意味着析构函数将在新线程运行"的语义(如 SQLite 连接只能被单线程访问的例子);
  3. 阅读 Examples 四象限 建立类型直觉;
  4. 进入 shared-state 模块 深入学习 ArcMutexRwLockSync 类型的实际用法;
  5. 若想深究 unsafe impl 的手动实现责任,可延伸阅读 unsafe traits 章节

小结

  • Sync 的正式定义:T: Sync 当且仅当 &T: Send,即"多线程同时访问安全"等价于"共享引用可跨线程传递";
  • Send / Sync 是 unsafe marker traits,编译器自动推导,也可手动实现但需自负其责;
  • 标准库类型可归入四个象限:ArcMutexAtomic*Sender 等属于 Send + SyncCellRefCellReceiver 属于 Send + !SyncMutexGuard 属于 !Send + SyncRc 与裸指针属于 !Send + !Sync
  • Mutex<T>impl<T: Send> Sync blanket 实现,是"锁弥补了 T 的不可共享性"的典型范式;
  • 依靠这套类型系统,Rust 将大量数据竞争从运行时错误提前到编译期,这正是 fearless concurrency 的底气所在。
热门项目推荐
相关项目推荐

项目优选

收起
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.15 K
2.77 K
kernelkernel
deepin linux kernel
C
34
18
docsdocs
暂无描述
Markdown
900
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
929
1.85 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.36 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.94 K
1.03 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.47 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
534
603
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
398
leetcodeleetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
Markdown
77
23