【Rust】17-Send、Sync 与并发安全抽象

Send、Sync 与并发安全抽象

研究目标

  • 理解 SendSync 如何表达跨线程安全。
  • 区分所有权转移、共享引用和内部可变性。
  • 掌握 Rust 并发抽象背后的类型约束。

Rust 并发安全的基础

Rust 的并发安全不是来自某个单独的锁库,而是所有权、借用和类型系统共同作用的结果。核心原则是:

  • 数据可以被移动到另一个线程,但必须安全。
  • 数据可以被多个线程共享引用,但共享访问必须安全。
  • 可变共享需要同步原语或原子操作。

SendSync 是两个标记 trait,用于表达这些性质。

Send

Send 表示一个类型的值可以安全地转移到另一个线程。

rust 复制代码
use std::thread;

fn main() {
    let text = String::from("hello");

    let handle = thread::spawn(move || {
        println!("{text}");
    });

    handle.join().unwrap();
}

StringSend,所以可以通过 move 闭包转移到新线程。

反例是 Rc<T>

rust 复制代码
use std::rc::Rc;
use std::thread;

fn main() {
    let value = Rc::new(1);

    // thread::spawn(move || {
    //     println!("{value}");
    // });
}

Rc<T> 的引用计数更新不是线程安全的,所以它不是 Send

Sync

Sync 表示一个类型可以安全地被多个线程通过共享引用访问。形式化地说,如果 &TSend,那么 TSync

i32StringVec<T> 在合适条件下都是 Sync,因为多个线程共享不可变引用读取它们是安全的。

RefCell<T> 不是 Sync

rust 复制代码
use std::cell::RefCell;

fn main() {
    let value = RefCell::new(1);
    *value.borrow_mut() += 1;
}

RefCell<T> 的借用计数是非线程安全的运行时检查,不能被多个线程同时访问。

Arc 与 Mutex

跨线程共享所有权通常使用 Arc<T>

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

fn main() {
    let value = Arc::new(String::from("shared"));
    let cloned = Arc::clone(&value);

    let handle = thread::spawn(move || {
        println!("{cloned}");
    });

    println!("{value}");
    handle.join().unwrap();
}

如果需要跨线程修改共享数据,使用 Mutex<T>

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

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = Vec::new();

    for _ in 0..4 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            let mut guard = counter.lock().unwrap();
            *guard += 1;
        }));
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("{}", *counter.lock().unwrap());
}

Arc<Mutex<T>> 的含义是:多个线程共享同一个所有权句柄,每次修改前先获得互斥锁。

RwLock 与读多写少

RwLock<T> 允许多个读者或一个写者:

rust 复制代码
use std::sync::RwLock;

fn main() {
    let value = RwLock::new(vec![1, 2, 3]);

    {
        let read = value.read().unwrap();
        println!("{}", read.len());
    }

    {
        let mut write = value.write().unwrap();
        write.push(4);
    }
}

读多写少场景中,RwLock 可能比 Mutex 更合适。但具体性能取决于锁实现、竞争程度和临界区大小。

原子类型

简单计数器可以使用原子类型:

rust 复制代码
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;

fn main() {
    let counter = Arc::new(AtomicUsize::new(0));

    let mut handles = Vec::new();
    for _ in 0..4 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            counter.fetch_add(1, Ordering::Relaxed);
        }));
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("{}", counter.load(Ordering::Relaxed));
}

原子操作避免锁,但内存序选择很重要。Relaxed 只保证原子性,不建立跨线程同步顺序。复杂并发算法应谨慎使用原子并配合测试和模型检查。

Channel

另一种并发思路是消息传递:

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

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        tx.send(String::from("hello")).unwrap();
    });

    let message = rx.recv().unwrap();
    println!("{message}");
}

channel 通过移动消息所有权减少共享状态。很多系统可以优先用消息传递表达工作流,只在必要时共享可变状态。

自动实现与 unsafe impl

SendSync 通常由编译器自动推导。如果类型的所有字段都是 Send,该类型一般也是 SendSync 也类似。

手写 unsafe impl Sendunsafe impl Sync 意味着你向编译器承诺这个类型满足跨线程安全不变量:

rust 复制代码
struct MyPointer(*mut u8);

// unsafe impl Send for MyPointer {}

这非常危险。除非你完全理解内部别名、生命周期、同步和释放规则,否则不要手写这些实现。

async 中的 Send

异步任务也常遇到 Send

rust 复制代码
tokio::spawn(async move {
    // future 必须 Send + 'static
});

如果 future 跨 .await 保存了非 Send 值,整个 future 就不是 Send。解决方式包括改用 Arc、缩小非 Send 值作用域、使用本地任务执行器。

常见误解

  • Arc<T> 只提供线程安全引用计数,不让 T 自动可变。
  • Mutex<T> 保护的是数据访问,不是让逻辑自动无死锁。
  • Send 是所有权跨线程转移,Sync 是共享引用跨线程访问。
  • RefCell<T> 是单线程内部可变性,不是线程同步工具。

继续研究

  • Rustonomicon:Send and Sync。
  • Rust Book:fearless concurrency。
  • Rust Reference:marker traits、undefined behavior、data races。
  • Loom:并发模型测试工具。

后记

2026年6月11日15点23分于上海。

相关推荐
10mAh6 分钟前
【Git】误删分支、reset --hard 后提交丢了怎么办?——reflog、fsck 与安全恢复实战
数据库·git·安全·回归
DLYSB_21 分钟前
Kafka 消费倾斜死锁与 Partition 掉队:我用 Rust 写了个“数据管道物理哨兵”,比 Grafana 报警快了 18 秒
rust·kafka·grafana·报警灯
Suxing922 分钟前
C语言基础分享:从“租房”到“拆迁”,C语言动态内存管理
java·数据结构·算法
SomeB1oody33 分钟前
【RustyML入门】3.8. 正则化与归一化层
开发语言·后端·机器学习·rust·教程
LuminousCPP1 小时前
单链表专题(四)-刷题复盘篇-LeetCode 138 随机链表复制|原地拷贝法突破复杂指针操作
数据结构·笔记·算法·leetcode·链表
7177771 小时前
如何搭建信创研发交付体系?Gitee DevOps 国产化适配、安全与流水线能力解析
安全·gitee·devops
逆境不可逃1 小时前
【LeetCode 912】排序数组——随机化快速排序详解
数据结构·算法·排序算法
Coder-magician1 小时前
《代码随想录》刷题打卡day31:动态规划-背包问题part02
算法·动态规划
薛定e的猫咪1 小时前
从因果视角解决多智能体协作:细读 SCIC 算法
算法
c238561 小时前
《算法武林谱:四大排序神功与二分寻宝术全解》
数据结构·算法·排序算法