在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
// 实现了Send的类型组成的结构体,也是Send的
struct 用户信息 {
名字: String, // String是Send的
年龄: u8, // u8是Send的
分数: Vec
}
// 因此 用户信息 自动实现了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
// Arc
let shared = Arc::new(data);
// Mutex
// 这是Mutex设计的核心目的——包装一个可能不是线程安全的类型
let mutex_data: Mutex
// RwLock
let rw_data: RwLock
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
use std::rc::Rc;
let rc_value: Rc
// 你不能把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
unsafe impl
// 使用示例:验证 自定义锁 现在可以跨线程使用了
fn 跨线程使用锁
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
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 模拟异步调度
// 在真实的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
// 闭包可以被安全地调度到任意线程执行
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
第三个陷阱是锁的持有时间过长。在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
在性能优化方面,分片锁、最小化锁持有范围、以及避免死锁都是关键策略。当你的并发程序出现性能瓶颈时,优先考虑是否可以通过减少锁的粒度来提升并发度。
最后,记住一条核心原则:不要在不需要锁的时候持有锁,在不需要unsafe的时候使用unsafe。最小化锁的持有范围是最简单也最有效的性能优化手段,而谨慎使用unsafe impl则能最大程度保留编译器为你提供的安全保障。在追求性能的同时,始终确保你的Send和Sync实现是正确的——因为一旦出错,运行时的未定义行为将是你最难排查的问题。