rust语言学习笔记Trait(十七)Send、Sync(线程间数据所有权)

1、定义与语义

Send‌:表示类型的‌所有权‌可以在线程之间安全转移。
  • 如果 T: Send,则可以把 T 的值 move 到另一个线程。
  • 绝大多数基本类型(如 i32boolString)、标准库中的 Vec<T>Box<T> 等都实现了 Send
Sync‌:表示类型的‌共享引用‌可以在多个线程之间安全访问。
  • TSync 当且仅当 &T: Send
  • 如果一个类型 T 实现了 Sync,那么多个线程可以同时持有 &T 而不会引发数据竞争(即只读共享是安全的)。
  • 典型的 Sync 类型有 i32Mutex<T>(内部使用了同步原语)等。

一句话区分:

  • Send 管的是**"把值交给另一个线程"‌**(转移所有权)。
  • Sync 管的是 ‌**"把不可变引用同时给多个线程用"‌**(共享引用)。

2、 自动推导规则(auto trait)

编译器会按照‌结构体/枚举字段的组成 ‌自动为自定义类型实现 SendSync

  • 一个结构体(或枚举)的所有字段都实现了 Send,则该结构体自动实现 Send
  • 所有字段都实现了 Sync,则该结构体自动实现 Sync
  • 泛型类型 Foo<P>Send/Sync 实现取决于其类型参数 P 是否实现了相应 trait。

因此,只要你的类型完全由线程安全的组件构成,它就自动具备线程安全属性。

3、 常见的 SendSync 典型案例

表格

类型 Send Sync 说明
i32, f64, bool 等基础类型 原始类型天生线程安全
String, Vec<T>T: Send Send 部分组成
Box<T>T: Send ✅(若 T: Sync 参考实现依赖内部类型
Arc<T>T: Send + Sync ✅(若 T: Sync 原子引用计数,线程安全共享
Mutex<T>T: Send 内部有锁,可安全共享可变访问
RwLock<T>T: Send 读写锁,类似 Mutex
mpsc::Sender<T>T: Send 只能移动,不能多线程共享
Rc<T> 非原子引用计数,多线程下计数更新会竞争
RefCell<T>, Cell<T> ❌(单线程可用) 内部可变性无同步保护,跨线程共享引用可能导致数据竞争
*const T, *mut T 裸指针不提供任何安全保证

可以看到,Rc<T>RefCell<T> 是典型的非线程安全类型,因为它们的设计没有考虑并发场景。在需要跨线程的场景下,应该用 Arc<T> 替代 Rc<T>,用 Mutex<T>RwLock<T> 替代 RefCell<T>

4、 数值上的并发示例

(1)Send 示例:将数据 move 到新线程

rust 复制代码
use std::thread;

fn main() {
    let v = vec![1, 2, 3];                // Vec<i32> 实现了 Send
    let handle = thread::spawn(move || {  // 传递 v 的所有权给线程
        println!("{:?}", v);              // 所有权已移入线程
    });
    handle.join().unwrap();               // 等待线程完成
}

如果把 Vec 换成 Rc<T>,编译会报错:Rc 没有实现 Send,无法跨线程转移。

(2)Sync 示例:多线程共享只读数据

rust 复制代码
use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3]);     // Arc<Vec<i32>> 既 Send 又 Sync
    let mut handles = vec![];               // 存储线程句柄的向量
    for _ in 0..4 {
        let shared = Arc::clone(&data);     // 克隆 data,创建新的 Arc 实例
        // 每个线程都拥有 data 的一个引用,所以可以安全地访问 data
        handles.push(thread::spawn(move || {                // 传递 shared 的所有权给线程
            let sum: i32 = shared.iter().sum();             // 计算 shared 中所有元素的和
            println!("sum = {}", sum);
        }));
    }
    for h in handles {          // 等待所有线程完成
        h.join().unwrap();      // 等待线程完成
    }
}

这里 Arc 允许多个线程共享同一份不可变数据,Vec<i32> 实现了 Sync,所以安全。

(3)需要可变共享时,配合 Mutex

rust 复制代码
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0u64));   // 共享的计数器
    let mut handles = vec![];                   // 存储线程句柄的向量
    for _ in 0..8 {
        let c = Arc::clone(&counter);           // 克隆 counter,创建新的 Arc 实例
        handles.push(thread::spawn(move || {    // 传递 c 的所有权给线程
            for _ in 0..100_000 {
                *c.lock().unwrap() += 1;        // 加锁后修改 counter 的值
            }
        }));
    }
    for h in handles {                          // 等待所有线程完成
        h.join().unwrap();                      // 等待线程完成
    }
    println!("count = {}", *counter.lock().unwrap());   // 打印 counter 的值
}

Mutex<T> 自身实现了 Sync,即使内部 T 不是 Sync,也能通过锁提供线程安全的内部可变性。

5、手动实现 Send / Sync 的注意事项

SendSync 是 ‌unsafe trait ‌,这意味着手动实现它们需要用 unsafe impl,程序员必须自己保证相关的并发安全承诺:

  • 错误地实现 Send 可能导致一个线程在另一个线程仍在访问数据时将其释放,产生悬垂指针。
  • 错误地实现 Sync 可能允许多个线程同时对同一内存位置进行无同步的修改,引发数据竞争和未定义行为。

绝大多数情况下,你‌不需要也绝对不应轻易 ‌手动实现这两个 trait。应该依赖编译器自动推导,或者使用已经正确封装好的标准库类型(如 ArcMutexAtomic* 等)来组合出安全的并发结构。

rust 复制代码
use std::ptr::NonNull; // 引入 NonNull 指针类型,用于存储非空指针

struct MyBox<T>(NonNull<T>); // 定义一个 MyBox 结构体,用于存储非空指针

unsafe impl<T> Send for MyBox<T> {} // 实现 Send 特征,允许 MyBox 在线程之间发送
unsafe impl<T> Sync for MyBox<T> {} // 实现 Sync 特征,允许 MyBox 在线程之间共享

impl<T> MyBox<T> {
    fn new(x: *mut T) -> Option<Self> { // 创建一个 MyBox 实例
        // 检查指针是否为空,如果为空则返回 None,否则返回 Some(MyBox) 实例
        NonNull::new(x).map(|p| Self(p))
    }

    unsafe fn get(&self) -> &T { // 获取 MyBox 中存储的指针指向的值
        unsafe { &*self.0.as_ptr() } // 从指针中获取引用
    }
}

fn main() {
    let x = MyBox::new(&mut 100 as *mut i32); // 创建一个 MyBox 实例,指向 100

    // 检查 MyBox 是否为空
    if let Some(ref my_box) = x { // 如果 MyBox 不为空,ref 关键字表示借用 MyBox 实例
        // 从 MyBox 中获取引用
        let y = unsafe { my_box.get() };
        println!("{}", y);
    } else {
        println!("MyBox::new 返回了 None");
    }
    
    // 将 MyBox 实例 x 传递给线程内使用
    let thread = std::thread::spawn(move || {
        if let Some(my_box) = x {
            let y = unsafe { my_box.get() };
            println!("{}", y);
        } else {
            println!("MyBox::new 返回了 None");
        }
    });
    // 等待线程执行完成
    thread.join().unwrap();
}

6、总结

  • Send :类型所有权可跨线程传递,大部分类型自动实现;RcRefCell、裸指针除外。
  • Sync :类型共享引用可跨线程安全访问;T: Sync&T: Send
  • 编译器根据字段组成自动推导 Send/Sync,正确组合标准库中的线程安全容器即可满足绝大多数并发场景。
  • 多线程共享数据的典型模式:Arc<Mutex<T>>Arc<RwLock<T>>;单线程通信用 mpsc 通道。
  • 手动实现这两个 trait 属于 unsafe 操作,一般只出现在编写底层同步原语或 FFI 封装时。

Rust 的 SendSync 将线程安全的保证从运行时提前到编译期,让"无畏并发"真正落地。

相关推荐
云边有个稻草人30 分钟前
复盘小站:用 Doubao-Seed-Evolving,把厚厚的笔记一键变成可复习、可自测、可批改的知识资产
笔记·doubao·ai开发实战·复盘小站
程序员爱钓鱼1 小时前
Rust 生命周期案例详解:从编译错误到真实业务场景
前端·后端·rust
不会代码的小猴10 小时前
21. 泛型编程上
开发语言·c++·笔记·算法
摇滚侠12 小时前
《SpringBoot 3:入门与应用实战》第 2 章 IOC 思想与实现 阅读笔记 1
spring boot·笔记·后端
蒸蒸yyyyzwd12 小时前
秋招不熟悉力扣学习笔记1
笔记·学习·leetcode
LuminousCPP13 小时前
数据结构-双向循环链表
c语言·数据结构·笔记·链表
小弥儿13 小时前
GitHub今日热榜 | 2026-08-11:图原生基础设施首日登顶
学习·开源·github
302wanger15 小时前
【阅读笔记】当AI拿走一切之后
笔记
疯狂打码的少年16 小时前
【数据结构】串(字符串):基本概念与存储
数据结构·笔记
for_ever_love__16 小时前
python基础语法学习: 数据容器
windows·python·学习