文章摘要
该文章介绍了Rust异步编程中的"futurelock"死锁问题:当Future A持有的资源被Future B需要时,若负责这两个Future的任务不再轮询A,就会导致死锁。文章通过代码示例展示了这种微妙的风险场景,说明在编写异步Rust时需要特别注意这种情况。
文章总结
未来锁(Futurelock)问题解析
核心概念
未来锁是一种特殊的死锁现象,发生在异步Rust编程中。当以下条件同时满足时会出现: 1. 任务T正在等待未来F1完成(直接通过await等待) 2. 未来F1需要获取由未来F2持有的共享资源(如互斥锁) 3. 未来F2需要任务T轮询它才能释放资源,但T此时只轮询F1
典型示例
通过tokio::select!宏演示的经典场景:
rust
async fn do_stuff(lock: Arc<Mutex<()>>) {
let mut future1 = do_async_thing("op1", lock.clone()).boxed();
tokio::select! {
_ = &mut future1 => { /* 分支1 */ }
_ = sleep(Duration::from_millis(500)) => {
do_async_thing("op2", lock.clone()).await; // 分支2
}
};
}
这个程序会可靠地死锁,因为:
1. 后台任务持有锁5秒
2. select!先轮询future1(因锁被占返回Pending)
3. 500ms后选择分支2执行,此时future1仍在锁等待队列中
4. 后台任务释放锁后,future1获得锁但永远不会被轮询
关键机制
- 公平互斥锁:tokio::sync::Mutex按先进先出顺序分配锁,必须交给最早等待的future1
- select!行为:一旦某个分支就绪,其他分支的轮询会被放弃
- 任务调度:同一个任务负责多个未来时,可能停止轮询关键未来
常见触发场景
- 在select!分支中使用await
- 通过&mut future引用传递未来
- 使用FuturesOrdered/FuturesUnordered时在next()后await其他未来
- 手动实现Future时出现类似逻辑
调试难点
- 表现为程序挂起,难以通过常规手段诊断
- 在omicron#9259案例中表现为:
- 所有请求阻塞在容量为1的mpsc通道发送端
- 接收端却显示通道为空
- 根本原因是发送端未来陷入未来锁,无法完成发送
防范建议
通用原则:
- 当单个任务并发轮询多个未来时,确保不停止轮询已启动的未来
- 优先考虑spawn新任务而非共享任务
使用select!时:
rust // 安全做法:将future生成独立任务 let future1_task = tokio::spawn(do_async_thing("op1", lock.clone())); tokio::select! { _ = &mut future1_task => { /* ... */ } // ... }- 避免同时出现:&mut future引用 + 分支内await
- 用JoinHandle替代直接future引用
使用Stream时:
- 用tokio的JoinSet替代FuturesOrdered
- 在Stream循环体内避免await其他未来
通道使用:
- 避免依赖send().await的隐式无限队列
- 采用较大容量通道 + try_send()显式处理背压
反模式警示
- 盲目增大通道容量无法根本解决问题
- 试图消除未来间依赖关系不现实(依赖可能深藏调用栈)
待解决问题
- 能否通过clippy lint检测危险模式:
- select!中使用&mut future
- 在select!分支内使用await
安全影响
未来锁可能导致拒绝服务,但属于程序缺陷而非独立安全问题。
(注:原文中的代码示例、详细执行流程和部分技术讨论已精简保留核心内容,删除了重复说明和非关键细节)
评论总结
以下是评论内容的总结:
关于Rust异步设计的讨论
- 有评论质疑Rust为何选择async而非更清晰的actor模型(如Erlang):
- "what made you decide to go for the async design pattern instead of the actor pattern, which - to me at least - seems so much cleaner"
- "Ever since I started using Erlang it felt like I finally found 'the right way'"
- 有评论质疑Rust为何选择async而非更清晰的actor模型(如Erlang):
关于futurelock问题的技术分析
- 多位开发者讨论futurelock与同步锁的相似性及解决方案:
- "futurelock is similar to keeping a sync lock across an await point"
- "cancellation is really two different things...the future is holding a guard"
- 有观点认为这是Tokio库的问题而非Rust语言问题:
- "the crux of the problem is the
tokio::select!macro, it seems like a pretty clear tokio bug"
- "the crux of the problem is the
- 多位开发者讨论futurelock与同步锁的相似性及解决方案:
关于异步编程复杂性的讨论
- 有开发者表达对异步代码复杂性的担忧:
- "async code is so so so much more complex...It's so hard to read and rationalize"
- "async code is supposed to make code simpler! But I'm increasingly unconfident that's true"
- 有开发者表达对异步代码复杂性的担忧:
技术细节讨论
- 关于select!宏的行为细节:
- "the
select!macro cancels the other branches by dropping them" - "dropping a reference in Rust doesn't do anything"
- "the
- 与操作系统优先级反转的类比:
- "This sounds very similar to priority inversion...I wonder if there is a similar idea possible with tokio"
- 关于select!宏的行为细节:
正面评价
- 多位评论者赞赏文章的清晰解释:
- "Great read, and the example code makes sense"
- "Great blogpost...Very insidious"
- 多位评论者赞赏文章的清晰解释:
其他语言对比
- 有评论提到JS和Concurrent ML:
- "curious to see if this could happen on JS"
- "Concurrent ML solved this issue"
- 有评论提到JS和Concurrent ML: