HarmonyOS掌上记账APP开发实践第16篇:Worker 多线程与异步编程 — ArkTS 并发方案在记账应用中的实践

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.etsrefreshBill() -> checkBudgetOverrun() 的异步链展示了典型的数据库操作流程。d:\HarmonyOS\WorkSpace\MoneyTrack1.0.3\features\assets\src\main\ets\viewmodels\AssetsVM.etsdeleteAsset() 方法展示了 Promise 链式调用和统一错误处理。

最佳实践

  1. 数据库操作统一 async :所有涉及数据库增删改查的方法都应声明为 async,返回 Promise,以便调用方能够通过 await 等待操作完成,避免回调地狱。
  2. 错误集中处理 :定义一个全局的异常处理工具(如 WindowUtil.dealAllError),所有异步操作的异常统一汇聚到该工具中,避免分散的 try/catch 导致代码冗余。
  3. Worker 任务粒度控制:创建 Worker 的开销较大(约几十毫秒),应仅在耗时超过 100ms 的计算密集型任务中使用,轻量操作直接在主线程用 async/await 完成即可。
  4. 合理选择并发模式 :无依赖的并行任务用 Promise.all,有先后依赖的串行任务用 async/await,计算密集型任务用 Worker。
  5. 及时释放 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 参考
相关推荐
全堆鸿蒙2 小时前
鸿蒙应用开发实战【37】— 数据统计:countByStatus 聚合查询
数据库·华为·harmonyos·鸿蒙系统
拥抱太阳06162 小时前
HarmonyOS 应用开发《掌上英语》第15篇:泛型编程在 ArkTS 中的应用:BreakpointType 源码解读
华为·harmonyos
绝世番茄2 小时前
鸿蒙原生 ArkTS 布局方式之 Swiper + itemSpace 卡片间距布局实战(API 24)
华为·harmonyos·鸿蒙
ldsweet2 小时前
《HarmonyOS技术精讲-Basic Services Kit(基础服务)》开篇:全景概览与核心价值
华为·harmonyos
心中有国也有家2 小时前
AtomGit Flutter 鸿蒙客户端:MoodStorage 的 CRUD 集成验证
android·服务器·flutter·华为·harmonyos
FrameNotWork3 小时前
HarmonyOS 6.0 WebView嵌入与交互
华为·交互·harmonyos
b130538100493 小时前
鸿蒙应用开发实战【41】— 登录页面LoginPage完整开发
服务器·华为·harmonyos·鸿蒙系统
2501_918582373 小时前
鸿蒙应用开发实战【31】— CardDao 卡号增删改查完整实现
华为·harmonyos·鸿蒙系统
全堆鸿蒙3 小时前
鸿蒙应用开发实战【36】— 数据查询优化:predicates 链式调用
数据库·harmonyos·鸿蒙系统