命令有了契约,本节解决"命令活在什么环境里":应用级状态如何注入命令、多个窗口同时调用时共享数据怎么保护、耗时任务在哪跑才不冻住界面。这一站是造芯的夹具与动力工序——并发安全的地基打在这里。
"应用状态"指活满整个进程生命周期的数据:数据库连接池、配置缓存、笔记的内存索引。Tauri 的机制叫受管状态——启动时把值交托给应用,命令用类型签名领取:
use std::sync::Mutex; use serde::{Deserialize, Serialize}; #[derive(Default)] pub struct AppState { pub notes: Mutex<Vec<Note>>, // 内存索引,互斥访问 pub db: Mutex<Option<DbHandle>>, // 数据库连接,启动后填充 } pub fn run() { tauri::Builder::default() .manage(AppState::default()) // 托管:状态进入应用容器 .invoke_handler(tauri::generate_handler![add_note]) .run(tauri::generate_context!()) .unwrap(); } #[tauri::command] fn add_note(state: tauri::State<AppState>, title: String) -> Result<Note, String> { let mut notes = state.notes.lock().map_err(|_| "状态锁中毒")?; let note = Note::new(title); notes.push(note.clone()); Ok(note) }
三个细节读透:参数里的 state 不来自前端——它由框架按类型从容器里取出,前端无需也不应传;锁在拿句柄、不在用数据——lock 只保护 push 那一行,I/O 别放在锁内,否则一把锁串行化整个应用;锁中毒要处理——持锁线程 panic 后锁进入中毒态,map_err 转成业务错误比 unwrap 崩溃体面。
Mutex 还是 RwLock? 多读少写(读多写少的索引、配置)用 RwLock,读并发不被写锁拖住;写多或临界区短用 Mutex,语义简单不易错。拿不准就 Mutex——先正确后优化。

命令标成 async 后,它在 tokio 运行时的异步线程池上执行:
#[tauri::command] async fn fetch_remote_note(id: u64) -> Result<Note, String> { let body = reqwest::get(remote_url(id)) .await .map_err(|e| format!("网络错误: {e}"))? .text() .await .map_err(|e| format!("读取错误: {e}"))?; parse_note(&body) }
await 期间线程让出去服务别的调用,界面与其它命令不被网络等待拖累——这是 async 命令的价值。但 async 不解决 CPU 密集:解析 50 MB 文本这类纯计算活,在 async 上下文里跑会把异步线程占死,其他 await 全堵。分流规则一句话:等 I/O 用 async,算 CPU 用 spawn_blocking:
#[tauri::command] async fn build_index(state: tauri::State<'_, AppState>) -> Result<usize, String> { let notes = state.notes.lock().map_err(|_| "锁中毒")?.clone(); // 重活扔进阻塞线程池,不占异步线程 let count = tauri::async_runtime::spawn_blocking(move || { heavy_index_build(¬es) // 纯计算:分词、倒排、压缩 }) .await .map_err(|e| format!("任务失败: {e}"))?; Ok(count) }
把 tokio 心智简化成一张图:异步线程池(少量线程跑大量等待型任务,禁止长计算)与阻塞线程池(按需扩容跑重活,禁止轻率滥用)。命令默认跑在前者;spawn_blocking 显式投递到后者。还有第三个去处——后台长任务(文件监视、定时同步):在 Setup 里 spawn 出去,独立生命周期,与命令无关地长期运行(第 7 章托盘、第 8 章更新检查都这么写)。三处的分流判断只问一个问题:这个活的瓶颈是等待还是计算?
并发纪律的必要性用一个故障讲透。随身笔记迭代到多窗口版本后出现灵异现象:偶发地,整个应用点什么都不响应,日志停在"保存中"。排查路径值得复现一遍。症状:偶发、无报错、重启恢复——典型的死锁面孔。诊断:审查所有同时拿两把锁的代码,发现保存命令先锁 notes 再锁 db,而自动备份任务先锁 db 再锁 notes——两个执行流以相反次序申请同样的两把锁,交错时互相等待。修复:统一全项目的加锁次序(先 db 后 notes),保存命令重排为先取 db、后取 notes;更稳妥的变体是把"取数据、写数据"合成一个函数,锁的次序只在这一处出现。解读:死锁的防不住靠的不是小心,是约定——加锁次序写成一行注释钉在状态定义旁边,所有新命令照着写;再配一条"拿第二把锁前想想手里有没有第一把"的自检口诀。
顺带一个同类陷阱:在持有 Mutex 的情况下 await。异步命令里锁守卫活在 await 点两侧,任务调度交错时极易长期持锁,把别的调用堵在门外。规矩:锁的守卫绝不跨 await——需要异步操作时,先在同步段取出数据克隆,drop 锁,再 await。Rust 编译器对部分情形会报错拦截,但它拦不住所有形态(比如守卫被塞进结构体带过去),纪律仍是第一道防线。
夹具与动力装好。下一节开能力接口:文件、网络、进程三大件怎么封装得守规矩。