mcpField
A
async-rust

async-rust

github.com/rewrite-rs开发1044 次安装11 周前更新
代码仓库

关于 async-rust

面向编写异步代码的 Rust 开发者,聚焦并发正确性、执行器阻塞、跨 await 的锁使用及取消安全,并指导运行时与共享状态的选择。

主要功能

  1. 01并发与取消正确性
  2. 02避免阻塞执行器线程
  3. 03处理跨 await 的锁
  4. 04运行时与共享状态指导
  5. 05使用 Clippy 和测试验证异步代码

适用场景

  1. 01检查 Rust 异步代码中的死锁风险
  2. 02处理多步骤操作的取消
  3. 03为任务通信选择通道
  4. 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)] 覆盖变更路径。

评论

支持 Markdown