异步命令与后台任务
下载、索引、批量处理这些耗时操作绝不能写进同步命令,否则整个应用都会卡住。Tauri 命令支持 async fn 形式,配合 tokio 运行时可以轻松写出高吞吐后台任务。本篇讲异步命令写法、任务取消与进度通知的完整模式。
一、异步命令基础
把命令声明为 async fn 即可,Tauri 会自动在异步运行时上执行它:
#[tauri::command]
async fn fetch_count() -> Result<u32, String> {
let resp = reqwest::get("https://api.example.com/count")
.await
.map_err(|e| e.to_string())?;
let n: u32 = resp.json().await.map_err(|e| e.to_string())?;
Ok(n)
}
异步命令不阻塞主线程,前端 invoke 依然是异步等待。需要 HTTP 能力时引入 reqwest crate,并在 Capability 中声明网络权限,具体权限配置见「权限模型:Capability 与作用域」。
| 命令类型 | 阻塞主线程 | 适用场景 |
|---|---|---|
| 同步 fn | 是 | 微秒级快速操作 |
| async fn | 否 | 网络、文件等 IO |
| spawn 后台任务 | 否 | 长时间运行、进度推送 |
注意同步命令内部也不要做长时间 sleep,会冻结窗口事件循环。
二、后台任务与 tokio
需要独立后台循环时,用 tokio::spawn 或标准库线程。用应用句柄在任务中发送事件:
use tauri::{AppHandle, Emitter};
use std::time::Duration;
#[tauri::command]
async fn start_sync(app: AppHandle) -> Result<(), String> {
tokio::spawn(async move {
for i in 1..=100 {
tokio::time::sleep(Duration::from_millis(200)).await;
let _ = app.emit("sync-progress", i);
}
let _ = app.emit("sync-done", ());
});
Ok(())
}
AppHandle 是克隆友好的,move 进任务后仍能发事件。后台任务持有句柄不会阻止应用退出;反过来如果任务必须随应用退出而终止,需要自行管理生命周期。
三、任务取消
未受控的后台任务是个隐患。实践上有两种取消方案。第一种用共享的取消标志:
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[tauri::command]
async fn start_and_monitor(app: AppHandle, state: tauri::State<'_, AppState>) {
let cancel = state.cancel_flag.clone();
tokio::spawn(async move {
loop {
if cancel.load(Ordering::SeqCst) { break; }
tokio::time::sleep(Duration::from_secs(1)).await;
let _ = app.emit("tick", ());
}
});
}
#[tauri::command]
fn cancel_sync(state: tauri::State<'_, AppState>) {
state.cancel_flag.store(true, Ordering::SeqCst);
}
第二种更优雅:用 tokio_util::sync::CancellationToken 或 tokio::select! 监听取消信号,代码更清晰,具体 API 以 tokio 文档为准。无论哪种,核心思想是把「需要外部停下的任务」做成可观测、可中断的循环。
| 取消方案 | 优点 |
|---|---|
| AtomicBool 标志 | 简单直观 |
| CancellationToken | 支持异步等待 |
| select! 多路复用 | 可同时监听多个信号 |
state 中存放取消标志的写法见「全局状态:tauri::State 与共享数据」。
四、进度通知的完整模式
把命令、事件、后台任务串起来,就得到通用的「长任务」骨架:前端调命令启动任务,命令立即返回任务 id;任务在后台推进,不断 emit 带 id 的进度;前端按 id 匹配 UI。
import { invoke } from '@tauri-apps/api/core'
import { listen } from '@tauri-apps/api/event'
const taskId = await invoke('start_sync')
await listen('sync-progress', (e) => {
if (e.payload.id === taskId) {
updateBar(e.payload.percent)
}
})
多任务并行时 id 区分很重要,否则进度会串线。事件订阅的完整写法与注意事项见「事件系统:Rust 主动通知前端」。
五、异步陷阱清单
| 陷阱 | 表现 | 处理 |
|---|---|---|
| 跨 await 持锁 | 死锁或编译失败 | 先 clone 数据再 await |
| 忘 await | 返回 Future 而非值 | 检查返回类型 |
| 阻塞式 IO 在 async 中 | 拖慢调度 | 用 spawn_blocking 包裹 |
| 未取消的任务 | 事件持续触发 | 注册取消机制 |
异步是 Rust 里最有深度的话题之一,本篇只覆盖 Tauri 常用部分。想系统学习异步与并发模型,继续读「所有权与线程安全:Send、Sync 与 Mutex」;Rust 语法基础见 Rust 入门;用 AI 辅助排查异步死锁问题可参考 Claude Code 指南,日志批量分析的技巧见 正则 mastery。
六、异步调试工具箱
异步代码出问题时,光看代码很难定位,需要借助工具。常用组合:
| 工具 | 用途 |
|---|---|
| tracing | 结构化日志带任务上下文 |
| console-subscriber | tokio 任务实时面板 |
| tokio-console | 可视化观察任务调度 |
| 打印任务 id | 区分并发请求的处理路径 |
console-subscriber 是 tokio 官方的调试利器,能在运行时看到每个任务的等待时间与唤醒次数,定位「为什么这个任务跑了十秒」这类问题非常直接,具体用法以 tokio 文档为准。
最后给一个尽快上手的心法:先用同步代码把逻辑写对,再逐块改造为 async。一开始就追求全异步,往往陷入借用与生命周期报错的泥潭。先正确再并发,是 Rust 学习曲线最平缓的走法。
异步是 Tauri 中高级开发的分水岭。先从单命令 async 化开始,再把后台任务与进度事件串起来,最后引入取消机制,循序渐进即可。并发安全的相关知识在下一章展开。
一个小结:async 让命令不阻塞,spawn 让任务脱离请求生命周期,事件让任务结果抵达前端,取消机制让任务可控收尾。四件套备齐,后台任务这一章就算真正落地。并发安全是下一章的内容。
异步三部曲收束成一句口诀:命令 async 化、任务 spawn 化、进度事件化。三者顺序固定依次引入即可,不需要一上来追求完美架构。
延伸阅读
- 所有权与线程安全:Send、Sync 与 Mutex——锁与并发的原理
- 事件系统:Rust 主动通知前端——进度推送的消费端
- Rust 入门:async 与 await 的语法基础
