在 Rust 中,通常情况下一个值只能有一个所有者(Owner)。但在实际开发(尤其是 Web 开发)中,我们经常需要多个地方同时持有同一个数据。
比如在 Actix-web 中,你的数据库连接池或全局配置需要被每一个线程、每一个请求处理器(Handler)共享。这时候,单一所有权就不够用了。
为了解决这个问题,Rust 提供了多所有权 机制,核心工具是 Rc 和 Arc。
1. 核心工具:引用计数(Reference Counting)
多所有权的本质不是"复制数据",而是 "共享数据,并记录有多少人在用它"。
Rc<T> (Reference Counted)
- 适用场景:单线程环境。
- 原理 :你在堆上存一份数据,每当有人想要所有权,计数器就 <math xmlns="http://www.w3.org/1998/Math/MathML"> + 1 +1 </math>+1;有人不用了,计数器就 <math xmlns="http://www.w3.org/1998/Math/MathML"> − 1 -1 </math>−1。当计数器归零时,数据自动销毁。
- 缺点:不是线程安全的。
Arc<T> (Atomic Reference Counted)
- 适用场景 :多线程环境(Actix-web 开发中最常用)。
- 原理:使用"原子操作"来更新计数器,确保在多线程同时修改计数时不会出错。
- 代价 :比
Rc稍微重一点,但在 Web 并发环境下是必须的。
2. 形象的比喻:合租房的钥匙
-
普通所有权:一间房只有一把钥匙,你要进去,前一个人必须把钥匙彻底给你(Move)。他给你后,他就进不去了。
-
多所有权 (Arc) :房间还是那间房,但我们可以配很多把钥匙。
- 每配一把(
.clone()),登记簿上的"人数"就 <math xmlns="http://www.w3.org/1998/Math/MathML"> + 1 +1 </math>+1。 - 每个人离开时把钥匙还回去,人数就 <math xmlns="http://www.w3.org/1998/Math/MathML"> − 1 -1 </math>−1。
- 最后一个人还钥匙并离开时,灯才会灭,房间(内存)才会被回收。
- 每配一把(
3. 在 Actix-web 中的实战用法
在 Actix-web 中,你几乎每天都在隐式或显式地使用 Arc。最典型的例子就是 web::Data。
Rust
rust
use actix_web::{web, App, HttpServer};
use std::sync::Arc;
struct AppState {
app_name: String,
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// 1. 创建共享状态
// web::Data 内部其实就封装了一个 Arc
let shared_data = web::Data::new(AppState {
app_name: String::from("MyActixApp"),
});
HttpServer::new(move || {
App::new()
.app_data(shared_data.clone()) // 2. 这里在为每个线程"配钥匙"
.route("/", web::get().to(index))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
4. 关键点:多所有权不代表"可变"
这是初学者最容易混淆的地方:Arc 本身只让你"看",不让你"改"。
- 如果你有多把钥匙进入房间(
Arc),你们默认只能进去参观。 - 如果其中一个人想进去装修(修改数据) ,必须配合内部可变性 工具:
Mutex(互斥锁)或RwLock(读写锁)。
所以,在 Rust 后端开发中,你经常会看到这种"叠罗汉"的写法:Arc<Mutex<T>>
意思是:一个可以多线程共享(Arc)的、能锁住进行修改(Mutex)的数据。
5. 总结
| 工具 | 多所有权? | 线程安全? | 常用场景 |
|---|---|---|---|
Box<T> |
❌ 否 | ❌ 否 | 堆上分配数据,单一所有权 |
Rc<T> |
✅ 是 | ❌ 否 | 单线程内的复杂数据共享 |
Arc<T> |
✅ 是 | ✅ 是 | Web 开发、多线程共享状态、数据库连接池 |
1). Box<T>:基础的堆内存分配
场景:当你有一个非常大的结构体,不希望在函数调用时在栈 (Stack) 上频繁拷贝;或者你需要定义递归类型。
rust
struct BigData {
data: [u8; 1024 * 1024], // 1MB 的大数据
}
fn main() {
// 将大数据放入堆(Heap)中,栈上只保留一个指针
let boxed_data = Box::new(BigData {
data: [0; 1024 * 1024],
});
// 传递时非常快,只移动指针
process_data(boxed_data);
}
fn process_data(data: Box<BigData>) {
println!("处理了 {} 字节的数据", data.data.len());
}
2). Rc<T>:单线程内的多处共享
场景:在 GUI 开发或单线程逻辑中,有多个对象需要引用同一个配置,且没有线程安全压力。
rust
use std::rc::Rc;
struct Config {
theme: String,
}
fn main() {
let common_config = Rc::new(Config {
theme: "Dark".to_string(),
});
// 克隆 Rc,增加计数,不拷贝数据本身
let component_a = Rc::clone(&common_config);
let component_b = Rc::clone(&common_config);
println!("当前配置引用数: {}", Rc::strong_count(&common_config)); // 输出 3
println!("组件 A 的主题: {}", component_a.theme);
}
3). Arc<T>:Actix-web 中的多线程状态共享
场景:这是你在学习 Actix-web 时最常用的。它让多个 worker 线程能安全地访问同一个全局状态(比如数据库连接、全局计数器)。 注意:如果要修改数据,通常配合 Mutex 使用。
rust
use actix_web::{web, App, HttpServer, Responder};
use std::sync::{Arc, Mutex};
// 定义全局状态
struct AppState {
// Arc 保证多个线程都能持有这个计数器
// Mutex 保证同一时刻只有一个线程能修改它
visitor_count: Arc<Mutex<u32>>,
}
async fn index(data: web::Data<AppState>) -> impl Responder {
// 1. 先锁住数据
let mut count = data.visitor_count.lock().unwrap();
// 2. 解引用并修改
*count += 1;
format!("你是第 {} 位访客", count)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// 创建共享状态
let app_state = AppState {
visitor_count: Arc::new(Mutex::new(0)),
};
// 包装成 Actix 的 Data 类型(它内部也会处理 Arc)
let data = web::Data::new(app_state);
HttpServer::new(move || {
App::new()
.app_data(data.clone()) // 每个线程都拿到一个 Arc 的副本
.route("/", web::get().to(index))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
核心区别总结:
-
Box:独占所有权。就像你买了一本书放在家里,只有你能看。
-
Rc:单线程多所有权。就像在家里(单线程),你和爸妈共享一台电视机,看电视的人数为 0 时电视才关掉。
-
Arc:多线程多所有权。就像在公司(多线程),大家共用一台饮水机。因为有多个人(线程)可能同时去接水,所以需要特殊的"原子操作"来记录人数,确保安全。