把重活搬出主线程:@Concurrent + TaskPool 执行 CPU 密集任务与进度回传

前言

HarmonyOS 的 UI 渲染跑在主线程(Actor 模型,每个 UIAbility 一个主线程)。如果你在主线程里做"大数组排序"“解析大 JSON”“图片像素处理”“递归计算”,主线程被占满,界面就会掉帧、点击无响应。正确做法是把这些任务丢到 TaskPool 工作线程,用 @Concurrent 装饰器标记可并发执行的函数。本文讲清 @Concurrent 的约束、如何回传进度、以及如何取消长任务——这些都是官方文档一笔带过、实际开发必踩的点。

问题描述

常见翻车现场:

  1. 在按钮点击里同步跑一个 50 万次的循环计算,页面直接"假死" 2 秒,体验灾难。
  2. 想给耗时任务加进度条,但在并发函数里改不了 @State,因为 @Concurrent 函数不能访问外部变量、不能碰 UI
  3. 任务跑了一半,用户退出了页面,后台还在算,浪费资源且可能引发状态更新到已销毁组件。
  4. 试图把 class 实例、function 当参数传进 TaskPool,直接报"参数不可序列化"。

细节解析

@Concurrent 函数的硬性约束

  • 必须 import { taskpool } from '@kit.ArkTS',函数前加 @Concurrent 装饰器。
  • 不能捕获/引用外部作用域的变量,所有输入必须来自参数;所有输出必须靠 returntaskpool.Task 通信。
  • 参数与返回值必须是**可序列化(Serializable)**类型。大二进制建议用 ArrayBuffer 并配合 transferable(所有权转移,零拷贝)。
  • 内部不能调用任何 UI / ArkUI API。

两种执行方式

  1. taskpool.execute(func, ...args) —— 一次性任务,返回 Promise<结果>。最简单。
  2. new taskpool.Task(func, ...args) + taskpool.execute(task) —— 可重复执行、可取消、可通信(taskpool.sendData / onReceiveData)。

进度回传与取消

  • @Concurrent 函数里用 taskpool.sendData(progress) 周期性抛出进度。
  • 主线程 task.onReceiveData((data) => updateProgress(data)) 接收,更新 @State
  • 取消:taskpool.cancel(task)。注意:TaskPool 是协作式取消,函数内部应检查 taskpool.isCanceled() 及时退出。

示例代码

一、最简单:一次性计算(斐波那契/大数)

// src/main/ets/worker/heavy.ets
import { taskpool } from '@kit.ArkTS'

@Concurrent
function fib(n: number): number {
  if (n < 2) return n
  let a = 0, b = 1
  for (let i = 2; i <= n; i++) {
    const t = a + b
    a = b
    b = t
  }
  return b
}

export async function calcFibInPool(n: number): Promise<number> {
  // execute 直接返回 Promise
  return await taskpool.execute(fib, n)
}

// 调用方(UI)
// try {
//   const r = await calcFibInPool(40)
//   console.info('结果', r)
// } catch (e) { console.error(e) }

二、带进度条 + 可取消的长任务

// src/main/ets/worker/process.ets
import { taskpool } from '@kit.ArkTS'

@Concurrent
function processLargeArray(total: number): number {
  let sum = 0
  for (let i = 0; i < total; i++) {
    // 协作式取消:用户退出时及时收手
    if (taskpool.isCanceled()) {
      console.info('[worker] 任务被取消,提前退出')
      return sum
    }
    sum += Math.sqrt(i) * Math.sin(i)
    // 每 5% 回传一次进度
    if (i % Math.max(1, Math.floor(total / 20)) === 0) {
      taskpool.sendData(Math.floor((i / total) * 100))
    }
  }
  taskpool.sendData(100)
  return sum
}

export function createProcessTask(total: number): taskpool.Task {
  return new taskpool.Task(processLargeArray, total)
}
// src/main/ets/pages/WorkerPage.ets
import { taskpool } from '@kit.ArkTS'
import { createProcessTask } from '../worker/process'

@Entry
@Component
struct WorkerPage {
  @State progress: number = 0
  @State result: number = 0
  private task: taskpool.Task | null = null

  start() {
    this.progress = 0
    this.task = createProcessTask(5_000_000)
    // 接收子线程进度
    this.task.onReceiveData((data: number) => {
      this.progress = data
    })
    taskpool.execute(this.task)
      .then((r: number) => { this.result = r })
      .catch((e: Error) => { console.error('任务失败/取消', e) })
  }

  cancel() {
    if (this.task) {
      taskpool.cancel(this.task) // 触发子线程 isCanceled() 退出
      this.task = null
    }
  }

  build() {
    Column({ space: 16 }) {
      Text(`进度: ${this.progress}%`)
      Progress({ value: this.progress, total: 100 })
        .width('80%')
      Button('开始计算(后台线程)').onClick(() => this.start())
      Button('取消').onClick(() => this.cancel())
      Text(`结果: ${this.result.toFixed(2)}`)
    }
    .width('100%').height('100%').justifyContent(FlexAlign.Center)
  }
}

三、零拷贝传大数组(Transferable)

@Concurrent
function transform(buf: ArrayBuffer): number {
  const view = new Float64Array(buf)
  let s = 0
  for (let i = 0; i < view.length; i++) s += view[i]
  return s
}

export async function runWithTransfer(size: number): Promise<number> {
  const buf = new ArrayBuffer(size * 8)
  const view = new Float64Array(buf)
  for (let i = 0; i < size; i++) view[i] = i
  // 第三个参数把 buf 标记为 transferable:所有权转移给子线程,无拷贝
  return await taskpool.execute(transform, buf, { transferList: [buf] })
}

总结

  1. 重活一定出主线程:CPU 密集计算用 taskpool.execute,别在主线程同步跑。
  2. @Concurrent 是"纯函数":不碰外部变量、不碰 UI,输入输出全靠参数/返回值。
  3. 进度条用 sendData + onReceiveData,参数用可序列化类型;大二进制用 transferList 零拷贝转移所有权。
  4. 可取消任务用 Task 对象 + taskpool.cancel,内部靠 taskpool.isCanceled() 协作退出,避免算到一半退出页面还空转。
  5. Worker 与 TaskPool 取舍:一次性小任务用 execute(func);需重复/取消/通信的长任务用 Task 对象。不要用 Worker 文件(重量级)做这种轻量并发。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐