关于 async-rust
面向编写异步代码的 Rust 开发者,聚焦并发正确性、执行器阻塞、跨 await 的锁使用及取消安全,并指导运行时与共享状态的选择。
主要功能
- 01并发与取消正确性
- 02避免阻塞执行器线程
- 03处理跨 await 的锁
- 04运行时与共享状态指导
- 05使用 Clippy 和测试验证异步代码
适用场景
- 01检查 Rust 异步代码中的死锁风险
- 02处理多步骤操作的取消
- 03为任务通信选择通道
- 04用 Clippy 和测试检查异步改动
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rewrite-rs/ skills --skill async-rust安装后在 Claude Code 中输入/async-rust确认已加载。
附带文件agents/openai.yamlCANCELLATION.md
详细介绍
异步 Rust
异步 Rust 常在三个方面出错:执行器线程被阻塞、锁保护器跨越 await 点持有,以及 Future 在操作中途被丢弃。本技能关注并发与取消情况下的正确性——不决定代码是否应该首先采用异步。
异步并不意味着免费并发
async fn 会生成一个 Future,只有在被轮询时才会执行。并发来自运行时以及 Future 的组合——join!、select!、spawn——而不是来自 async 关键字:
// Serial: the second fetch starts only after the first completes.
let a = fetch_user(id).await;
let b = fetch_orders(id).await;
// Concurrent: both futures are polled at the same time.
let (a, b) = tokio::join!(fetch_user(id), fetch_orders(id));
函数中的每个 .await 都会增加生成的 Future 的大小,而大型 Future 每次移动时都会被复制——例如移入 Box::pin 或 JoinSet。大量 await 和大型局部变量会占用数千字节;只有在性能分析表明移动开销确实重要时,才装箱内部 Future 或拆分函数。
在顶层选择运行时,一次即可
审慎选择一个运行时;绝大多数情况下应选 tokio。库的规则是:尽可能保持运行时无关,无法做到时就用 feature 控制集成,并且绝不要在库函数中启动运行时(#[tokio::main] 应用于二进制程序或测试;在那里调用 Runtime::block_on 会给使用者带来第二个相冲突的执行器)。
异步上下文中的阻塞工作
async fn 中的 CPU 密集型循环、同步文件或网络调用,或不包含 .await 的计算过程,都会阻塞执行器线程,使该线程上的所有其他任务无法运行。解决方法:阻塞式 I/O 和短时 CPU 工作使用 tokio::task::spawn_blocking,持续的 CPU 工作使用专用线程池(rayon),并且绝不要在任务中调用 std::thread::sleep。对于计算循环,定期让出执行权是效果较弱的解决办法——只有分块确实足够小时才有帮助。
Send、Sync 与 spawn 的约束
tokio::spawn 要求 Send + 'static:跨越 .await 持有的所有内容都必须是 Send——这会导致 Rc 和 RefCell 无法通过编译;而跨越 .await 持有 std::sync::MutexGuard 则可能造成死锁:
// Deadlock waiting to happen: the std guard is held across the await.
let mut guard = state.lock().unwrap();
guard.value = fetch().await;
// Either drop the guard before awaiting…
let value = fetch().await;
state.lock().unwrap().value = value;
// …or use the async-aware mutex when the lock genuinely must span the await.
let mut guard = state.lock().await; // tokio::sync::Mutex
guard.value = fetch().await;
除非锁必须跨越 .await,否则优先使用 std::sync::Mutex,这种情况下使用 tokio::sync::Mutex——clippy::await_holding_lock 能捕获常见问题。
取消安全性
任何 Future 都可能在任意 .await 点被丢弃——这正是 select! 和超时的工作方式。如果一个 Future 已消耗输入,却尚未提交,那么它在执行过程中被丢弃时就会丢失数据。规则是:将不可逆步骤放在一个不可取消的整体中执行,或者让操作可以恢复。具体案例见 CANCELLATION.md。
跨任务共享状态
共享修改使用 Arc<Mutex<T>>,任务间传递工作使用通道;如果状态只有一个逻辑所有者,则使用消息传递——由任务拥有自身状态并接收操作指令,胜过其他任务直接访问共享内存。
| 通道 | 用途 |
|---|---|
mpsc | 多个生产者向一个消费者发送消息 |
oneshot | 单次回复 |
watch | 获取最新值,且允许遗漏中间值 |
broadcast | 每个接收者都能看到每条消息,且积压消息数量有上限 |
选择取决于语义,而非性能——本应使用 watch 却选了 broadcast,会导致接收者落后并产生意料之外的错误。
动态任务组及其中止语义见 CANCELLATION.md。
公共 API 中的异步
除非需要额外控制,否则优先使用 async fn 而非 -> impl Future——反糖形式还会以调用者所依赖的方式承诺 Send 性(或不具备 Send 性)。在 trait 中,async fn 仍缺少用于 Send 约束的语法糖,而 #[async_trait] 仍是支持对象安全异步 trait 的实用选择。
延后处理
超时和取消的错误类型见 /rust-errors;共享所有权见 /ownership-not-clone;公共接口承诺见 /rust-api-design;手动实现 Future 时的 Pin 不变量见 /unsafe-rust。
验证
cargo clippy --all-targets # add -- -D warnings only if the repo has no lint config
cargo test
与异步相关的 lint(await_holding_lock、await_holding_refcell_ref、
async_yields_async)会从其输出中移除,而非启用。异步错误取决于时序——
cargo test 通过并不能提供充分证据:如果仓库使用 #[tokio::test],请使用
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] 覆盖变更路径。