把重活搬出主线程:@Concurrent + TaskPool 执行 CPU 密集任务与进度回传
·
把重活搬出主线程:@Concurrent + TaskPool 执行 CPU 密集任务与进度回传
前言
HarmonyOS 的 UI 渲染跑在主线程(Actor 模型,每个 UIAbility 一个主线程)。如果你在主线程里做"大数组排序"“解析大 JSON”“图片像素处理”“递归计算”,主线程被占满,界面就会掉帧、点击无响应。正确做法是把这些任务丢到 TaskPool 工作线程,用 @Concurrent 装饰器标记可并发执行的函数。本文讲清 @Concurrent 的约束、如何回传进度、以及如何取消长任务——这些都是官方文档一笔带过、实际开发必踩的点。
问题描述
常见翻车现场:
- 在按钮点击里同步跑一个 50 万次的循环计算,页面直接"假死" 2 秒,体验灾难。
- 想给耗时任务加进度条,但在并发函数里改不了
@State,因为@Concurrent函数不能访问外部变量、不能碰 UI。 - 任务跑了一半,用户退出了页面,后台还在算,浪费资源且可能引发状态更新到已销毁组件。
- 试图把
class实例、function当参数传进 TaskPool,直接报"参数不可序列化"。
细节解析
@Concurrent 函数的硬性约束
- 必须
import { taskpool } from '@kit.ArkTS',函数前加@Concurrent装饰器。 - 不能捕获/引用外部作用域的变量,所有输入必须来自参数;所有输出必须靠
return或taskpool.Task通信。 - 参数与返回值必须是**可序列化(Serializable)**类型。大二进制建议用
ArrayBuffer并配合transferable(所有权转移,零拷贝)。 - 内部不能调用任何 UI / ArkUI API。
两种执行方式
taskpool.execute(func, ...args)—— 一次性任务,返回Promise<结果>。最简单。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] })
}
总结
- 重活一定出主线程:CPU 密集计算用
taskpool.execute,别在主线程同步跑。 @Concurrent是"纯函数":不碰外部变量、不碰 UI,输入输出全靠参数/返回值。- 进度条用
sendData+onReceiveData,参数用可序列化类型;大二进制用transferList零拷贝转移所有权。 - 可取消任务用
Task对象 +taskpool.cancel,内部靠taskpool.isCanceled()协作退出,避免算到一半退出页面还空转。 - Worker 与 TaskPool 取舍:一次性小任务用
execute(func);需重复/取消/通信的长任务用Task对象。不要用 Worker 文件(重量级)做这种轻量并发。
更多推荐
所有评论(0)