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 将线程安全的保证从运行时提前到编译期,让"无畏并发"真正落地。

相关推荐
H__Rick1 小时前
C51学习-DAY7
单片机·嵌入式硬件·学习·51单片机
dtq04241 小时前
C语言刷题函数1-判断素数(分支语句,函数两种方法)
c语言·开发语言·学习
尘汐筠竹1 小时前
Day1-2 学习笔记:在 AMD 云环境上部署 Gemma 4 大模型
笔记·学习·datawhale·amdev
Litluecat1 小时前
配合多角色提示语4,学习AI漫剧(刚开始学)
人工智能·学习·计算机视觉
AOwhisky1 小时前
学习自测与解析:Redis系列第一期与第二期核心知识点详解
运维·数据库·redis·学习·云计算
逸模2 小时前
逸模 VS CAD+SU系列(三)工程量---逸模模型级智能算量,数据同源闭环 助力公装项目精准控本高效拓店
人工智能·笔记·算量·公装·构件库
zhangrelay2 小时前
个体智能大模型使用的主观数据复盘-节选-2026-
笔记·学习·课程设计
lunzi_08262 小时前
【学习笔记】《Python编程 从入门到实践》第9章:类、继承、组合与面向对象编程
笔记·python·学习
aXin_ya2 小时前
Ai Vibecoding学习(Claude Code的安装使用前置)
学习·vibe codeing