Send和Sync trait详解:构建线程安全的Rust并发数据结构

Cos赛事 2026-09-01 17:43:24 8903

在Rust语言中,编译器不会像Java那样在运行时检查线程安全问题,而是把这件事前置到编译阶段。这意味着你写出的代码如果存在数据竞争,编译器会直接报错,让你根本无法通过编译。实现这一机制的核心,就是Send和Sync这两个标记trait。

一、什么是Send和Sync

1.1 为什么需要它们

先抛开"标记trait"这个专业术语,用一个生活化的比喻来理解。假设你有一把刀,这把刀属于你一个人使用。如果你把刀交给别人,你自己就不能同时用了——这就是所有权转移。但如果两个人同时拿这把刀,就会发生冲突——这就是数据竞争。

Rust中的Send trait,解决的就是"这把刀能不能交给另一个人"的问题。如果一个类型实现了Send,就意味着它的值可以从一个线程安全地转移到另一个线程。

Sync trait解决的则是"这把刀能不能被两个人同时看"的问题。如果一个类型实现了Sync,就意味着它的引用可以安全地在多个线程之间共享。

这两个trait虽然叫"标记trait",看起来什么也不干,但实际上它们是Rust编译器进行线程安全检查的基石。编译器在编译你的代码时,会检查所有跨线程操作,确保涉及的数据类型满足Send或Sync的要求。

1.2 Send的本质

Send trait的定义极其简单:

/// 标记trait:表示类型的所有权可以从一个线程安全转移到另一个线程

pub trait Send {}

它的定义没有任何方法需要实现,但它所代表的含义非常明确:拥有这个类型的线程,可以安全地将它的所有权移交给另一个线程,而不需要任何同步机制。

举个例子,当你创建一个子线程并想把某个值传进去时:

// 技术栈:Rust

use std::thread;

fn main() {

// 创建一个字符串值

let name = String::from("Rust开发者");

// 将这个值移动到子线程中

// 因为String实现了Send,所以这个操作是合法的

let handle = thread::spawn(move || {

// name的所有权现在完全属于这个子线程

println!("子线程收到: {}", name);

});

// 等待子线程执行完毕

handle.join().unwrap();

}

如果name的类型没有实现Send,编译器会直接拒绝这段代码,并告诉你不能跨线程传递这个值。

1.3 Sync的本质

Sync trait同样定义极其简单:

/// 标记trait:表示类型的引用可以在多个线程间安全共享

pub trait Sync {}

如果类型T实现了Sync,那么&T(即T的不可变引用)就实现了Send。这个推导关系是编译器内置的,不需要你手动声明。

理解Sync的一个直观方式:如果多个线程需要读取同一个数据(而不是转移它的所有权),那么只有当这个数据类型实现了Sync,编译器才会允许。

在实际场景中,通常需要配合Arc来实现多线程共享同一个数据的所有权:

// 技术栈:Rust

use std::thread;

use std::sync::Arc;

fn main() {

// 使用Arc包装,允许多个线程共享同一个数据的所有权

let config = Arc::new("数据库连接池配置".to_string());

let mut handles = vec![];

for i in 0..3 {

// 克隆Arc,而不是克隆内部数据

// Arc的引用计数会让所有线程都能安全访问内部数据

let config_clone = Arc::clone(&config);

handles.push(thread::spawn(move || {

// 通过Arc访问内部数据,不会发生数据竞争

println!("线程{}读取配置: {}", i, config_clone);

}));

}

for handle in handles {

handle.join().unwrap();

}

}

二、Send和Sync的编译器自动推导

2.1 哪些类型自动实现Send

Rust标准库中的大多数基本类型都自动实现了Send和Sync,比如整数、浮点数、布尔值、字符类型等。判断规则其实很简单:如果一个类型的所有字段都实现了Send,那么这个类型本身也实现了Send。

// 技术栈:Rust

// 基本数值类型都是Send和Sync

let a: i32 = 42;

let b: f64 = 3.14;

let c: bool = true;

// 标准库的String、Vec、Box等智能指针都是Send和Sync

let s: String = "hello".to_string();

let v: Vec = vec![1, 2, 3];

// 实现了Send的类型组成的结构体,也是Send的

struct 用户信息 {

名字: String, // String是Send的

年龄: u8, // u8是Send的

分数: Vec, // Vec是Send的

}

// 因此 用户信息 自动实现了Send和Sync

2.2 哪些类型自动实现Sync

Sync的推导规则和Send类似,但有一个关键的细微差别:如果一个类型实现了Sync,那么它的不可变引用&T就实现了Send。这意味着你可以将&T的引用从A线程移到B线程,前提是T本身是Sync的。

// 技术栈:Rust

use std::sync::{Arc, Mutex, RwLock};

// 基本类型、String、Vec等和Send一样,也是Sync的

let data: Vec = vec![10, 20, 30];

// Arc在T: Send + Sync时,本身也是Send + Sync

let shared = Arc::new(data);

// Mutex无论T是什么类型,Mutex本身都是Send + Sync

// 这是Mutex设计的核心目的——包装一个可能不是线程安全的类型

let mutex_data: Mutex = Mutex::new("受保护数据".to_string());

// RwLock和Mutex类似,也是Send + Sync

let rw_data: RwLock> = RwLock::new(vec![0xFF]);

2.3 不自动实现的情况

并非所有类型都能自动获得Send和Sync。以下情况会导致类型失去这些标记:

// 技术栈:Rust

// 情况1:包含原始指针的自定义类型

struct 原生指针包装 {

ptr: *mut u8, // 原始指针不是Send也不是Sync

}

// 因为*mut u8不是Send/Sync,所以 原生指针包装 也不是

// 情况2:包含不实现Send/Sync的类型

use std::sync::atomic::Ordering;

struct 混合类型 {

安全字段: String, // 这个是Send + Sync

不安全字段: *const u8, // 这个不是Send也不是Sync

}

// 因为含有不安全字段,整个结构体都不安全

// 情况3:某些特定的标准库类型

// 比如 Rc 就不是Send和Sync的(因为它没有引用计数保护)

use std::rc::Rc;

let rc_value: Rc = Rc::new("这是跨不了线程的".to_string());

// 你不能把rc_value移到另一个线程中

三、手动实现Send和Sync

3.1 使用unsafe实现Send

在某些特殊场景中,你编写了一个类型,编译器无法自动判断它是线程安全的,但实际上它确实是安全的。这时你需要手动实现Send或Sync。

// 技术栈:Rust

// 这个结构体包含一个原始指针,编译器不知道它是否安全

// 但经过分析,我们知道这个指针指向的数据是线程安全的

struct 安全的原子指针 {

ptr: *mut u8,

}

// 手动实现Send,告诉编译器:这个类型可以安全地跨线程转移

// unsafe意味着我们承诺:这个实现确实是安全的

unsafe impl Send for 安全的原子指针 {}

使用unsafe实现Send或Sync时,你实际上是在向编译器保证:这个类型在所有字段上都是线程安全的。如果这个保证是错误的,程序在运行时可能出现未定义行为(undefined behavior),而编译器不会帮你检查。

3.2 使用unsafe实现Sync

// 技术栈:Rust

// 假设我们有一个封装了操作系统级同步原语的自定义类型

struct 自定义锁 {

inner: T,

lock_fd: i32, // 文件描述符,默认不是Send/Sync

}

// 虽然lock_fd本身不是Send/Sync,但我们知道操作系统保证了

// 文件描述符在多线程环境下是安全的(通过适当的系统调用)

unsafe impl Send for 自定义锁 {}

unsafe impl Sync for 自定义锁 {}

// 使用示例:验证 自定义锁 现在可以跨线程使用了

fn 跨线程使用锁(data: 自定义锁) {

use std::thread;

let handle = thread::spawn(move || {

// 因为 自定义锁 实现了Send,这个操作是合法的

let value = &data.inner;

println!("在子线程中访问锁内的数据");

});

handle.join().unwrap();

}

3.3 unsafe实现的风险

手动实现Send和Sync是一把双刃剑。好处是它给了你灵活性,让你可以处理标准库没有覆盖的场景。坏处是它绕过了编译器的安全检查,一旦出错,调试难度极大。

// 技术栈:Rust

use std::sync::Mutex;

// 这是一个错误的例子(仅用于说明,不要在实际代码中使用)

struct 危险类型 {

缓冲区: [u8; 256],

位置: usize,

}

// 下面的实现是错误的!因为缓冲区是可变数据,

// 如果两个线程同时修改它,就会发生数据竞争

// unsafe impl Send for 危险类型 {} // 绝对不要这样做!

// 正确的做法是使用Mutex保护共享数据

struct 安全的危险类型 {

缓冲区: Mutex<[u8; 256]>,

位置: Mutex,

}

// 现在这个类型自动是Send + Sync的,不需要手动实现

fn main() {

let data = 安全的危险类型 {

缓冲区: Mutex::new([0u8; 256]),

位置: Mutex::new(0),

};

use std::thread;

let mut handles = vec![];

// 线程1:修改缓冲区

let data_clone = std::sync::Arc::new(data);

let d1 = std::sync::Arc::clone(&data_clone);

handles.push(thread::spawn(move || {

let mut buf = d1.缓冲区.lock().unwrap();

buf[0] = 42;

let mut pos = d1.位置.lock().unwrap();

*pos = 1;

}));

// 线程2:读取缓冲区

let d2 = std::sync::Arc::clone(&data_clone);

handles.push(thread::spawn(move || {

let buf = d2.缓冲区.lock().unwrap();

println!("缓冲区首字节: {}", buf[0]);

}));

for h in handles {

h.join().unwrap();

}

}

四、构建线程安全的并发数据结构

4.1 使用Mutex包装类型

Mutex是最常用的线程同步原语。当多个线程需要访问同一个可变数据时,Mutex确保任意时刻只有一个线程可以访问这个数据。

// 技术栈:Rust

use std::sync::{Arc, Mutex};

use std::thread;

fn main() {

// 使用Arc>包装共享的可变数据

// Arc提供多所有权,Mutex提供互斥访问

let counter = Arc::new(Mutex::new(0));

let mut handles = vec![];

// 启动5个线程,每个线程对计数器加10次

for _ in 0..5 {

let counter_clone = Arc::clone(&counter);

let handle = thread::spawn(move || {

for _ in 0..10 {

// lock()返回MutexGuard,在作用域结束时自动释放锁

// 如果获取锁失败(比如死锁),unwrap会panic

let mut num = counter_clone.lock().unwrap();

*num += 1; // 对锁保护的数据进行修改

}

});

handles.push(handle);

}

// 等待所有线程完成

for handle in handles {

handle.join().unwrap();

}

// 所有线程完成后,读取最终结果

println!("计数器最终值: {}", *counter.lock().unwrap());

// 输出: 计数器最终值: 50

}

4.2 使用RwLock实现读写分离

当你的场景是"读多写少"时,RwLock比Mutex更高效。RwLock允许多个线程同时读取数据,但写入时仍然需要独占访问。

// 技术栈:Rust

use std::sync::{Arc, RwLock};

use std::thread;

fn main() {

// 共享的配置数据,允许多个线程同时读取

let config = Arc::new(RwLock::new("初始配置".to_string()));

let mut handles = vec![];

// 启动3个读线程

for i in 0..3 {

let config_clone = Arc::clone(&config);

handles.push(thread::spawn(move || {

// read()获取读锁,允许多个读线程同时执行

let data = config_clone.read().unwrap();

println!("线程{}读取配置: {}", i, data);

}));

}

// 启动1个写线程

let config_clone = Arc::clone(&config);

handles.push(thread::spawn(move || {

// write()获取写锁,写线程独占访问

let mut data = config_clone.write().unwrap();

*data = "更新后的配置".to_string();

println!("配置已更新: {}", data);

}));

// 等待所有线程完成

for handle in handles {

handle.join().unwrap();

}

}

4.3 构建自定义线程安全集合

通过组合Send和Sync的理解,你可以构建自己的线程安全数据结构。

// 技术栈:Rust

use std::collections::HashMap;

use std::sync::{Arc, Mutex};

/// 一个线程安全的键值存储结构

/// 内部使用HashMap存储数据,通过Mutex保证线程安全

pub struct 线程安全存储 {

data: Mutex>,

}

impl 线程安全存储 {

/// 创建新的存储实例

pub fn 新建() -> Self {

线程安全存储 {

data: Mutex::new(HashMap::new()),

}

}

/// 插入或更新一个键值对

/// 返回是否是新插入的键

pub fn 插入(&self, key: String, value: String) -> bool {

let mut map = self.data.lock().unwrap();

map.insert(key, value)

}

/// 获取指定键的值

/// 返回Option,键不存在时返回None

pub fn 获取(&self, key: &str) -> Option {

let map = self.data.lock().unwrap();

map.get(key).cloned()

}

/// 删除指定键的值

/// 返回被删除的值,键不存在时返回None

pub fn 删除(&self, key: &str) -> Option {

let mut map = self.data.lock().unwrap();

map.remove(key)

}

/// 返回当前存储的条目数量

pub fn 大小(&self) -> usize {

let map = self.data.lock().unwrap();

map.len()

}

}

fn main() {

// 创建共享的线程安全存储

let storage = Arc::new(线程安全存储::新建());

let mut handles = vec![];

// 多个线程同时写入不同的数据

for i in 0..5 {

let storage_clone = Arc::clone(&storage);

handles.push(thread::spawn(move || {

let key = format!("key_{}", i);

let value = format!("value_{}", i * 2);

storage_clone.插入(key, value);

println!("写入完成");

}));

}

// 等待写入完成

for handle in handles.drain(..) {

handle.join().unwrap();

}

// 多线程同时读取

let mut handles = vec![];

for i in 0..5 {

let storage_clone = Arc::clone(&storage);

handles.push(thread::spawn(move || {

let key = format!("key_{}", i);

if let Some(value) = storage_clone.获取(&key) {

println!("读取结果: {} = {}", key, value);

}

}));

}

for handle in handles {

handle.join().unwrap();

}

println!("存储大小: {}", storage.大小());

}

五、Send和Sync在实际项目中的应用

5.1 全局共享状态

在实际项目中,经常需要使用全局的共享状态。Rust提供了std::sync::LazyLock(Rust 1.80+)来实现延迟初始化的全局变量,这在旧版本中通常使用lazy_static宏。

// 技术栈:Rust

use std::sync::{RwLock};

use std::sync::LazyLock;

#[derive(Clone)]

struct 应用配置 {

最大连接数: usize,

超时时间_秒: u64,

调试模式: bool,

}

/// 全局的应用配置,线程安全

static 全局配置: LazyLock> = LazyLock::new(|| {

RwLock::new(应用配置 {

最大连接数: 100,

超时时间_秒: 30,

调试模式: false,

})

});

fn main() {

use std::thread;

let mut handles = vec![];

// 线程1:读取配置

handles.push(thread::spawn(|| {

let config = 全局配置.read().unwrap();

println!("最大连接数: {}", config.最大连接数);

}));

// 线程2:更新配置

handles.push(thread::spawn(|| {

let mut config = 全局配置.write().unwrap();

config.调试模式 = true;

println!("已开启调试模式");

}));

// 线程3:再次读取更新后的配置

handles.push(thread::spawn(|| {

let config = 全局配置.read().unwrap();

println!("调试模式: {}", config.调试模式);

}));

for handle in handles {

handle.join().unwrap();

}

}

5.2 线程池任务传递

在线程池中,任务需要从一个线程传递到工作线程执行。这就要求任务类型(通常是闭包或函数指针)必须实现Send。

// 技术栈:Rust

use std::thread;

use std::sync::{Arc, Mutex};

use std::sync::mpsc;

use std::time::Duration;

/// 一个简单的任务队列,演示Send要求

struct 任务队列 {

接收端: Arc>>>,

}

impl 任务队列 {

/// 创建任务队列,返回队列本身和发送端

pub fn 新建() -> (Self, mpsc::Sender>) {

let (tx, rx) = mpsc::channel();

(

任务队列 {

接收端: Arc::new(Mutex::new(rx)),

},

tx,

)

}

/// 启动一个工作线程来消费任务

pub fn 启动(&self) {

thread::spawn(move || {

let rx = self.接收端.lock().unwrap().clone();

// 任务必须是Send的,才能跨线程传递

for task in rx {

println!("开始执行任务");

task();

println!("任务执行完成");

}

});

}

}

fn main() {

let (queue, mut tx) = 任务队列::新建();

queue.启动();

// 发送任务到队列

// 注意:这里使用了move闭包,因为Box要求

// 闭包必须拥有它捕获的所有数据的所有权

tx.send(Box::new(|| {

println!("任务1: 执行数据库备份");

thread::sleep(Duration::from_secs(1));

})).unwrap();

tx.send(Box::new(|| {

println!("任务2: 清理过期缓存");

thread::sleep(Duration::from_secs(1));

})).unwrap();

tx.send(Box::new(|| {

println!("任务3: 发送告警通知");

})).unwrap();

// 等待任务执行完毕

thread::sleep(Duration::from_secs(5));

}

5.3 异步任务中的Send要求

在使用tokio等异步运行时时,Send要求变得更加重要。异步任务(Task)需要在不同的线程之间调度,因此它们必须实现Send。

// 技术栈:Rust

use std::sync::Arc;

use std::time::Duration;

use std::thread;

/// 这个函数演示了在异步上下文中,Send要求如何起作用

/// 模拟了tokio::spawn对Send的要求

fn 模拟异步调度(data: T) {

// 在真实的tokio中,spawn一个异步任务时:

// tokio::spawn(async move {

// // 这个闭包必须实现Send

// // 因为它可能在不同的线程上执行

// let shared = Arc::new(data);

// tokio::time::sleep(Duration::from_secs(1)).await;

// println!("处理数据");

// })

// 如果data包含不是Send的类型,编译会报错

// 模拟调度到不同线程

let mut handles = vec![];

// 创建多个共享引用

let count = 3;

let data = Arc::new(data);

for _ in 0..count {

let data_clone = Arc::clone(&data);

handles.push(thread::spawn(move || {

// 这个闭包捕获了data_clone

// 因为Arc>实现了Send,所以这个闭包也是Send的

// 闭包可以被安全地调度到任意线程执行

println!("被调度的任务: {:?}", data_clone);

}));

}

for handle in handles {

handle.join().unwrap();

}

}

fn main() {

// 用同步方式模拟异步调度的Send要求

let data = vec![100, 200, 300];

模拟异步调度(data);

println!("所有任务已在不同线程上完成调度");

}

六、技术优缺点分析

6.1 优点

Send和Sync机制的最大优势在于它把线程安全的检查前移到了编译阶段。这意味着你在运行时永远不会遇到数据竞争导致的崩溃或数据损坏。这种"要么编译通过,要么在编译阶段修复问题"的模式,比Java的synchronized或C++的手动加锁要安全得多。

第二个优点是零成本抽象。Send和Sync本身是标记trait,没有任何运行时开销。编译器只是利用这些标记来生成正确的代码,不会额外引入锁或其他同步机制。真正提供同步能力的结构(如Mutex、RwLock)的性能特征则由它们的实现决定,与Send/Sync标记无关。

第三个优点是组合性。当你的结构体由多个字段组成时,编译器会自动推导整个结构体是否实现了Send或Sync。你不需要手动声明,只需要确保每个字段都是安全的。这种自动推导让大型项目中的线程安全维护变得可管理。

第四个优点是工具链支持良好。rustc编译器会给出非常明确的错误信息,告诉你哪个类型不是Send或不是Sync,以及原因是什么。IDE和linter工具也能帮助你在编码阶段就发现问题。

6.2 缺点

最主要的缺点是灵活性受限。某些在某些语言中可以合法使用的模式,在Rust中可能被Send/Sync规则阻止。例如,在C++中你可以安全地将一个裸指针传递到另一个线程(虽然不安全但允许),但在Rust中编译器会直接拒绝。

另一个缺点是调试困难。当你收到编译器告诉你"类型X不是Send"的错误时,有时候这个错误的根本原因并不直观。特别是当你的类型包含一些间接引用(比如Box),编译器可能给你一串让人困惑的错误信息,需要逐层排查。

还有一个实际问题是,当你使用unsafe impl Send或unsafe impl Sync时,你失去了编译器的保护。这意味着后续代码的任何变更都可能引入数据竞争,而编译器不会告诉你。这种静默的安全性漏洞在大型项目中尤其难以发现和修复。

七、注意事项

7.1 常见陷阱

第一个常见陷阱是混淆Send和Sync。记住:Send是关于"值的移动",Sync是关于"引用的共享"。如果你需要把值从A线程移到B线程,检查Send;如果你需要多个线程同时读取同一个值,检查Sync。

第二个陷阱是忘记包装共享的可变数据。Arc只提供了多所有权,但不提供线程安全。如果T不是Sync的,你不能让多个线程同时访问Arc指向的数据。你需要Arc>或Arc>。

第三个陷阱是锁的持有时间过长。在Mutex或RwLock的保护下持有锁的时间越长,其他线程等待的时间就越长,并发性能就越差。应该在最小的作用域内持有锁:

// 技术栈:Rust

use std::sync::{Arc, Mutex};

use std::thread;

use std::time::Duration;

fn main() {

let data = Arc::new(Mutex::new(vec![1, 2, 3, 4, 5]));

// 错误做法:在不需要锁的时候也持有锁

let d1 = Arc::clone(&data);

let bad_handle = thread::spawn(move || {

let guard = d1.lock().unwrap();

// 在这里做了很多不需要锁保护的操作

// 这会严重降低并发性能

for _ in 0..10 {

thread::sleep(Duration::from_millis(10));

}

println!("锁内操作完成,数据长度: {}", guard.len());

// guard在这里才释放锁

});

// 正确做法:只在需要访问共享数据时短暂持有锁

let d2 = Arc::clone(&data);

let good_handle = thread::spawn(move || {

// 只需要短暂持有锁来获取所需信息

let length = {

let guard = d2.lock().unwrap();

guard.len()

};

// 锁在上面的作用域结束后已经释放

// 后续操作不需要持有锁,不会阻塞其他线程

thread::sleep(Duration::from_millis(50));

println!("锁外操作完成,数据长度: {}", length);

});

bad_handle.join().unwrap();

good_handle.join().unwrap();

}

7.2 性能考量

在使用Send和Sync构建并发数据结构时,性能是一个重要考量。Mutex和RwLock的获取和释放操作本身就有成本。如果你发现程序大部分时间在等待锁,可能需要重新设计数据结构。

以下是一些性能优化的思路和示例:

// 技术栈:Rust

use std::sync::RwLock;

/// 减少锁竞争的思路:使用分片锁

/// 将数据分成多个独立的分片,每个分片有自己的锁

/// 这样不同的线程可以访问不同的分片而不互相阻塞

struct 分片锁存储 {

分片: Vec>>,

}

impl 分片锁存储 {

/// 创建分片数为n的存储

pub fn 新建(分片数: usize) -> Self {

let 分片 = (0..分片数)

.map(|_| RwLock::new(Vec::new()))

.collect();

分片锁存储 { 分片 }

}

/// 根据键的哈希值选择对应的分片

/// 不同键可能映射到不同的分片,从而减少锁竞争

pub fn 插入(&self, 键: u64, 值: u64) {

let 索引 = (键 % self.分片.len() as u64) as usize;

let mut 分片 = self.分片[索引].write().unwrap();

分片.push(值);

}

/// 获取指定分片中所有的值

pub fn 获取分片(&self, 索引: usize) -> Vec {

let 分片 = self.分片[索引].read().unwrap();

分片.clone()

}

/// 获取所有分片的总条目数

pub fn 总大小(&self) -> usize {

let mut total = 0;

for shard in &self.分片 {

total += shard.read().unwrap().len();

}

total

}

}

fn main() {

// 创建一个有4个分片的存储

let 存储 = 分片锁存储::新建(4);

// 插入数据

for i in 0..100 {

存储.插入(i, i * 10);

}

// 读取各分片数据

for i in 0..4 {

let 数据 = 存储.获取分片(i);

println!("分片{}包含{}个条目", i, 数据.len());

}

println!("总条目数: {}", 存储.总大小());

}

另一个重要的注意事项是避免死锁。当你有多个Mutex或RwLock需要同时持有时,必须始终以相同的顺序获取锁,否则可能导致死锁。

// 技术栈:Rust

use std::sync::{Arc, Mutex};

use std::thread;

/// 有两个需要保护的共享资源

struct 共享资源 {

资源A: Mutex,

资源B: Mutex,

}

fn main() {

let 资源 = Arc::new(共享资源 {

资源A: Mutex::new(10),

资源B: Mutex::new(20),

});

let mut handles = vec![];

// 正确的做法:所有线程都按照相同的顺序获取锁

// 先获取 资源A,再获取 资源B

for i in 0..2 {

let r = Arc::clone(&资源);

handles.push(thread::spawn(move || {

// 始终以A -> B的顺序获取锁

let mut a = r.资源A.lock().unwrap();

let mut b = r.资源B.lock().unwrap();

// 在持有两个锁时进行原子操作

let sum = *a + *b;

*a = sum;

*b = sum * 2;

println!("线程{}: A={}, B={}", i, a, b);

}));

}

for h in handles {

h.join().unwrap();

}

}

八、文章总结

Send和Sync是Rust线程安全模型的两大基石。它们作为标记trait,本身不包含任何逻辑,却为编译器提供了判断线程安全性的关键信息。理解Send(值能否跨线程转移)和Sync(引用能否跨线程共享)的区别和联系,是编写正确的Rust并发程序的前提。

对于大多数标准类型,编译器会自动处理Send和Sync的推导,你几乎不需要手动干预。但在构建自定义的并发数据结构时,你可能会遇到编译器自动推导不够用的情况,这时就需要你理解这些trait的本质,知道什么时候应该使用unsafe impl,以及如何通过Mutex、RwLock等原语来包装非线程安全的类型。

在实际项目中,Arc>和Arc>是两个最常用的组合模式。前者适用于需要多线程写入的场景,后者适用于读多写少的场景。理解这两个模式的适用场景,能够帮助你构建既正确又高效的并发系统。

在性能优化方面,分片锁、最小化锁持有范围、以及避免死锁都是关键策略。当你的并发程序出现性能瓶颈时,优先考虑是否可以通过减少锁的粒度来提升并发度。

最后,记住一条核心原则:不要在不需要锁的时候持有锁,在不需要unsafe的时候使用unsafe。最小化锁的持有范围是最简单也最有效的性能优化手段,而谨慎使用unsafe impl则能最大程度保留编译器为你提供的安全保障。在追求性能的同时,始终确保你的Send和Sync实现是正确的——因为一旦出错,运行时的未定义行为将是你最难排查的问题。

站点统计