Worker 多线程与异步编程 --- ArkTS 并发方案在记账应用中的实践

概述
在移动应用中,长时间运行的计算任务或阻塞式 I/O 操作不应占用主线程,否则会导致界面卡顿、触摸响应延迟,甚至触发系统 ANR(应用无响应)弹窗。HarmonyOS 提供了多种并发编程方案,包括 Worker(多线程)、async/await(异步协程)和 Promise 链式处理。MoneyTrack 项目在数据处理、数据库查询和网络请求等场景中大量使用这些并发机制。本文分析其异步编程模式与并发设计。
为什么需要并发? 在记账应用中,用户期望每次操作都能得到即时反馈。例如,切换账单月份时触发的大量账单统计计算、导出 CSV 时的文件写入操作、从远端同步账户数据时的网络请求等------这些任务如果全部在主线程中串行执行,UI 将出现明显的"卡死"感。通过合理的并发设计,可以将耗时任务放到后台线程,确保 UI 线程始终能及时响应用户的触摸和滚动操作,从而大幅提升用户体验。
核心知识点
1. async/await 异步模式
ArkTS 的 async/await 是处理异步操作的标准方式。在 MoneyTrack 的 ViewModel 中,几乎所有涉及数据库操作的方法都使用 async/await:
typescript
public async refreshBill() {
await this.dataProcessing.accountProcessing.loadAccounts();
this.dataProcessing.getBillReport(this.checkedResource);
try {
emitter.emit({ eventId: 1 });
} catch (error) {
WindowUtil.dealAllError(error);
}
this.checkBudgetOverrun();
}
await 关键字暂停当前函数的执行,等待 Promise resolve 后继续执行下一行。这使得异步代码看起来像同步代码,极大地提升了可读性。
async/await 的错误传播
async 函数中的异常会隐式地将 rejected Promise 传递给调用者。如果 refreshBill() 被另一个 async 函数调用,错误会自动向上传播:
typescript
public async loadPageData() {
try {
await this.refreshBill(); // 如果 refreshBill 抛出异常,这里会捕获到
} catch (error) {
// 统一处理所有页面加载异常
WindowUtil.dealAllError(error);
}
}
这种"自动传递"特性让开发者可以在调用链的最顶层统一处理错误,而不需要在每个层级都编写 try/catch,大大简化了错误处理代码。
2. Promise 链式处理
在某些场景中,MoneyTrack 使用 Promise 的 .then() .catch() 链式调用。例如 AssetsVM 中的删除资产操作:
typescript
public async deleteAsset(id: number) {
contextUtil.getPromptAction()
?.showDialog({ ... })
.then(async (data) => {
if (data.index === 1) {
await AccountingDB.deleteAssetAccount(id);
await this.assetProcessing.getAssetReport(...);
}
}).catch((error: BusinessError) => {
WindowUtil.dealAllError(error);
});
}
弹窗 API 返回 Promise,后续的数据库操作和页面刷新通过链式调用串行执行,并通过 .catch() 统一处理异常。
3. Promise.all 并发执行
当需要同时发起多个相互独立的异步操作时,Promise.all 可以显著缩短总体等待时间:
typescript
public async loadDashboardData() {
// 三个数据源之间没有依赖关系,可以并发加载
const [accounts, budgets, bills] = await Promise.all([
this.accountProcessing.loadAccounts(), // 加载账户列表
this.budgetProcessing.loadBudgets(), // 加载预算数据
this.billProcessing.loadRecentBills(), // 加载近期账单
]);
// 所有数据加载完成后,一次性更新 UI
this.updateDashboard(accounts, budgets, bills);
}
适用场景 :仪表盘首页的多数据面板加载、批量导出时同时查询多个账期数据、同步多个远端 API 请求等。使用 Promise.all 时需要注意,任何一个 Promise 被 reject,整体结果就会进入 catch 分支,因此适合"全有或全无"的场景。
4. Worker 多线程
对于计算密集型的任务(如大数据量的账单统计),可以通过 Worker 创建新线程避免阻塞主线程。在 MoneyTrack 中,AccountingDB 的数据库操作封装为异步调用,底层的耗时操作通过 Worker 线程执行,确保 UI 流畅。
Worker 的创建与通信
Worker 线程与主线程通过消息机制进行通信。以下是典型的使用流程:
typescript
// 主线程:创建 Worker 并发送消息
const worker = new worker.ThreadWorker('entry/ets/workers/ReportWorker.ets');
worker.onmessage = (event: MessageEvents) => {
const result = event.data; // 接收 Worker 返回的处理结果
console.info(`统计完成: ${JSON.stringify(result)}`);
worker.terminate(); // 任务完成后销毁 Worker
};
worker.onerror = (error: ErrorEvents) => {
console.error(`Worker 出错: ${error.message}`);
worker.terminate();
};
// 向 Worker 发送待处理的数据
worker.postMessage({ type: 'generateReport', month: '2026-06', userId: 1001 });
// Worker 线程 (ReportWorker.ets)
workerPort.onmessage = (event: MessageEvents) => {
const { type, month, userId } = event.data;
// 执行耗时计算
const report = heavyCalculation(month, userId);
workerPort.postMessage(report); // 将结果发回主线程
};
主线程与 Worker 线程的交互流程
Worker 线程 主线程 (UI) Worker 线程 主线程 (UI) #mermaid-svg-qI2G2HVKHYPtq36f{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qI2G2HVKHYPtq36f .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qI2G2HVKHYPtq36f .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qI2G2HVKHYPtq36f .error-icon{fill:#552222;}#mermaid-svg-qI2G2HVKHYPtq36f .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qI2G2HVKHYPtq36f .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qI2G2HVKHYPtq36f .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qI2G2HVKHYPtq36f .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qI2G2HVKHYPtq36f .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qI2G2HVKHYPtq36f .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qI2G2HVKHYPtq36f .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qI2G2HVKHYPtq36f .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qI2G2HVKHYPtq36f .marker.cross{stroke:#333333;}#mermaid-svg-qI2G2HVKHYPtq36f svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qI2G2HVKHYPtq36f p{margin:0;}#mermaid-svg-qI2G2HVKHYPtq36f .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qI2G2HVKHYPtq36f text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-qI2G2HVKHYPtq36f .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qI2G2HVKHYPtq36f .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-qI2G2HVKHYPtq36f .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-qI2G2HVKHYPtq36f .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-qI2G2HVKHYPtq36f #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-qI2G2HVKHYPtq36f .sequenceNumber{fill:white;}#mermaid-svg-qI2G2HVKHYPtq36f #sequencenumber{fill:#333;}#mermaid-svg-qI2G2HVKHYPtq36f #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-qI2G2HVKHYPtq36f .messageText{fill:#333;stroke:none;}#mermaid-svg-qI2G2HVKHYPtq36f .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qI2G2HVKHYPtq36f .labelText,#mermaid-svg-qI2G2HVKHYPtq36f .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-qI2G2HVKHYPtq36f .loopText,#mermaid-svg-qI2G2HVKHYPtq36f .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-qI2G2HVKHYPtq36f .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qI2G2HVKHYPtq36f .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-qI2G2HVKHYPtq36f .noteText,#mermaid-svg-qI2G2HVKHYPtq36f .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-qI2G2HVKHYPtq36f .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qI2G2HVKHYPtq36f .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qI2G2HVKHYPtq36f .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qI2G2HVKHYPtq36f .actorPopupMenu{position:absolute;}#mermaid-svg-qI2G2HVKHYPtq36f .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-qI2G2HVKHYPtq36f .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qI2G2HVKHYPtq36f .actor-man circle,#mermaid-svg-qI2G2HVKHYPtq36f line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-qI2G2HVKHYPtq36f :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 进行大数据量计算(不阻塞 UI 线程) 用户触发操作(如切换月份)postMessage(计算任务参数)可继续响应其他操作postMessage(计算结果)接收结果并更新界面
UI 线程与 Worker 线程的数据传递限制
在 ArkTS 中,主线程与 Worker 线程之间的数据传递有明确的限制:
- 序列化传递:数据通过结构化克隆算法(Structured Clone)进行深拷贝,传递的是数据的副本而非引用,因此修改 Worker 中的数据不会影响主线程的原始数据。
- 限制类型:Function、Error、DOM 节点等无法传递。仅支持普通对象、Array、Map、Set、ArrayBuffer 等可序列化的类型。
- 性能考量:大数据量(如超过 100KB 的对象)的传递序列化开销较大,建议仅传递必要的最小数据集,或使用 ArrayBuffer 进行高效传输。
5. 异常处理策略
MoneyTrack 采用统一的异常处理策略:所有异步操作都可能抛出异常,通过 try/catch 或 .catch() 捕获后,统一交给 WindowUtil.dealAllError() 处理:
typescript
catch (error) {
WindowUtil.dealAllError(error);
}
这确保了错误被一致地记录和处理,不会导致应用崩溃。
项目案例
d:\HarmonyOS\WorkSpace\MoneyTrack1.0.3\features\home\src\main\ets\viewmodels\HomeVM.ets 中 refreshBill() -> checkBudgetOverrun() 的异步链展示了典型的数据库操作流程。d:\HarmonyOS\WorkSpace\MoneyTrack1.0.3\features\assets\src\main\ets\viewmodels\AssetsVM.ets 的 deleteAsset() 方法展示了 Promise 链式调用和统一错误处理。
最佳实践
- 数据库操作统一 async :所有涉及数据库增删改查的方法都应声明为
async,返回Promise,以便调用方能够通过await等待操作完成,避免回调地狱。 - 错误集中处理 :定义一个全局的异常处理工具(如
WindowUtil.dealAllError),所有异步操作的异常统一汇聚到该工具中,避免分散的 try/catch 导致代码冗余。 - Worker 任务粒度控制:创建 Worker 的开销较大(约几十毫秒),应仅在耗时超过 100ms 的计算密集型任务中使用,轻量操作直接在主线程用 async/await 完成即可。
- 合理选择并发模式 :无依赖的并行任务用
Promise.all,有先后依赖的串行任务用async/await,计算密集型任务用 Worker。 - 及时释放 Worker 资源 :Worker 任务完成后应调用
worker.terminate()或worker.close()释放线程资源,避免内存泄漏。
总结
HarmonyOS 为 ArkTS 应用提供了层次丰富的并发编程方案:async/await 让异步代码的编写和阅读如同同步代码般直观;Promise.all 让无依赖的并行任务高效协同;Worker 线程则解决了计算密集型任务阻塞 UI 的问题。在 MoneyTrack 的实践中,这三种方案各司其职------数据库操作和 UI 交互以 async/await 为主,多数据源加载使用 Promise.all,大数据量统计则由 Worker 在后台处理。理解并合理运用这些并发机制,是构建流畅、可靠 HarmonyOS 应用的关键能力。
参考文档
- ArkTS 并发编程指南(Worker)
- async/await 异步编程最佳实践
- Promise API 参考