Actix 源码解析:从 Mutex 困境到 Actor 模型
你的代码里有多少把 Mutex? 锁是并发编程的「必需品」,却也是 bug 的温床——死锁、活锁、优先级反转,每一个都让人头疼。问题的根源在于:共享可变状态 + 并发 = 复杂度不可控。 1973 年,Carl Hewitt 提出了 Actor 模型,给出了另一种思路:既然共享状态是问题根源,那就不共享。Erlang 把这个思想发扬光大,用 6 个基本函数(spawn、send、receive、register、whereis、self)构建了支撑电信系统「九个九」可用性的并发基础设施。 在 Rust 生态中,Actix 给出了一个有趣的答案:用类型系统来「编码」Actor 模型的约束,把运行时错误转化为编译期错误。 本文不是 Actix 的使用教程,而是一次源码探索。核心问题是:当我们调用 addr.send(msg) 时,背后到底发生了什么? 注意:本文讨论的是 Actix(Actor 框架),而不是 Actix-web(Web 框架)。它们是两个独立的 crate。 第一部分:Actor 模型的本质 传统并发的困境 先看一个「经典」的并发代码: use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } } 表面上没问题,但这段代码藏着几个深层问题: 问题 1:锁的传染性 一旦用了 Mutex,它就会像病毒一样传播。假设你有一个 UserService,它持有 Mutex<HashMap<UserId, User>>。现在你要加一个缓存,于是又多了 Mutex<Cache>。然后你发现需要在更新用户时同时更新缓存——两把锁,需要同时持有。 // 线程 A:更新用户时同步缓存 fn update_user(&self, user: User) { let users = self.users.lock().unwrap(); // 1. 先锁 users let cache = self.cache.lock().unwrap(); // 2. 再锁 cache // ... } // 线程 B:刷新缓存时检查用户 fn refresh_cache(&self) { let cache = self.cache.lock().unwrap(); // 1. 先锁 cache let users = self.users.lock().unwrap(); // 2. 再锁 users ← 死锁! // ... } 如果时序不巧,线程 A 持有 users 等待 cache,而线程 B 恰好持有 cache 等待 users——Loss(死锁)!这种 bug 最阴险:测试时可能跑 1000 次都正常,生产环境高并发时突然卡死。 ...