前言

在移动应用开发中,流畅度就是生命线。用户的手指在屏幕上滑动的每一毫秒,系统都需要在 16ms 内完成一帧画面的渲染。

如果我们的主线程(也就是 UI 线程)被任何耗时的操作堵塞,哪怕只是读取一个稍大的文件,或者进行一次复杂的数学运算,界面就会立刻掉帧,甚至完全卡死。这种体验对于用户来说是灾难性的。

在 Java 或 C++ 的传统开发中,我们习惯了直接 new Thread 或者使用线程池来分担任务,但在鸿蒙 HarmonyOS 6 (API 20) 的 ArkTS 引擎中,并发模型发生了一些根本性的变化。

ArkTS 采用了 Actor 并发模型,这意味着线程之间没有共享内存,也没有那令人头秃的锁机制。今天,我们就来聊聊如何在这个新模型下,利用 TaskPoolWorker 优雅地处理耗时任务,把主线程的宝贵资源还给 UI 渲染。

一、 理解 Actor 模型:这一座座孤岛

要掌握鸿蒙的并发,首先得扭转一个观念:线程之间不再能随意访问同一个变量了。在传统的共享内存模型中,多个线程可以同时读写同一个全局变量,为了防止数据错乱,我们不得不引入各种锁(Lock)和同步机制,这往往是死锁和竞态条件的温床。

而 ArkTS 采用的 Actor 模型,将每一个线程(无论是主线程、TaskPool 线程还是 Worker 线程)都视为一个独立的 Actor。你可以把它们想象成一个个独立的“房间”,每个房间里有自己的内存空间。

A 房间的人想要把数据给 B 房间,不能直接把手伸过去,而必须通过“发消息”的方式,把数据复制一份传递过去。这种设计的最大好处就是 天然无锁,彻底杜绝了数据竞争带来的 Bug,极大地提高了系统的稳定性。但代价就是,我们在设计代码时,必须时刻关注数据的传递成本。

二、 TaskPool:随叫随到的特种部队

在 API 20 中,鸿蒙官方强烈推荐首选 TaskPool(任务池) 来处理并发任务。你可以把它看作是一个智能的网约车调度平台。

当你有一个耗时任务(比如图片滤镜处理)时,你不需要自己去创建线程,也不需要关心线程的生命周期,只需要把任务包装好,扔给 TaskPool。系统会自动根据当前的负载情况、任务优先级,安排一个空闲的线程来执行它。

使用 TaskPool 的核心在于 @Concurrent 装饰器。我们需要将耗时的逻辑封装成一个独立的函数,并打上这个标记。这个函数必须是纯函数或者静态方法,不能依赖外部的 UI 组件状态(因为它是要被发送到另一个线程去执行的)。

当我们调用 taskpool.execute 时,系统会将函数的参数序列化后拷贝到工作线程,执行完毕后,再将结果序列化拷贝回主线程。这种机制非常适合那些 独立、短时、高 CPU 消耗 的任务,比如大图压缩、复杂算法计算等。TaskPool 会自动进行负载均衡,当任务过多时会自动扩容,空闲时会自动缩容,完全不需要开发者操心。

三、 Worker:常驻后台的专职管家

既然有了 TaskPool,为什么还需要 Worker?TaskPool 虽好,但它本质上是任务导向的,执行完就释放。如果你的应用需要一个长时间运行的后台线程,比如需要一直保持一个 WebSocket 长连接,或者需要一个常驻的数据库读写句柄,那么 TaskPool 就不太合适了。

Worker 更像是一个你专门雇佣的“全职员工”。你需要手动创建它(new worker.ThreadWorker),它拥有独立的文件上下文(worker.ts),并且会一直存活直到你显式地调用 terminate 销毁它。Worker 适合处理那些 生命周期较长、状态需要保持 的场景。在 Worker 线程中,我们通过 postMessage 向主线程发送消息,主线程通过 onmessage 接收。

虽然流程比 TaskPool 繁琐一些,但它提供了更精细的线程控制能力。需要注意的是,Worker 的数量是有限制的(通常最多 8 个),且创建和销毁都有一定的资源开销,所以千万不要滥用。

四、 通信机制:从“拷贝”到“转移”

前面提到,Actor 模型的数据通信依赖于序列化和反序列化,也就是 拷贝(Structured Clone)

对于普通的 JSON 对象或小数据,这种开销几乎可以忽略不计。但如果你要传递一张 10MB 的位图数据,或者一个巨大的 Float32Array,拷贝带来的 CPU 和内存消耗就是巨大的,甚至可能抵消多线程带来的性能红利。

为了解决这个问题,鸿蒙引入了 Transferable Object(转移对象) 的概念。对于 ArrayBuffer 这类二进制数据,我们可以选择“转移”控制权,而不是拷贝。就像我把手里的公文包直接递给你,我这里没有了,你那里有了,中间不需要复印文件。

在代码中,我们在 postMessage 或者 TaskPool 的参数中,可以将这些对象标记为 Transferable。一旦转移,原线程就无法再访问这块内存了(访问会报错),从而实现了 零拷贝(Zero Copy) 的极速通信。这是在处理音视频流、图像处理等大数据场景下的必杀技。

五、实战示例

下面拟了一个“图片高斯模糊处理”的耗时场景。我们在主界面上放了一个加载圈,通过 TaskPool 在后台线程进行数亿次的数学运算,你会发现主界面的加载圈依然转得丝滑流畅,完全没有被卡顿。

import { taskpool } from '@kit.ArkTS';
import { promptAction } from '@kit.ArkUI';

// -------------------------------------------------------------
// 1. 定义并发任务函数
// -------------------------------------------------------------
// 必须使用 @Concurrent 装饰器
// 这个函数将在独立的线程中运行,不能访问外部的 this 或 UI 状态
@Concurrent
function heavyImageProcess(buffer: ArrayBuffer, iterations: number): ArrayBuffer {
  // 模拟耗时操作:例如对图片像素进行复杂的矩阵运算
  const startTime = Date.now();
  
  console.info(`[TaskPool] 任务开始执行`);

  // 这里我们用空循环模拟 CPU 密集型计算
  // 实际场景中这里是对 buffer 进行像素级操作
  let result = 0;
  for (let i = 0; i < iterations; i++) {
    result += Math.sqrt(i) * Math.sin(i);
  }

  const endTime = Date.now();
  console.info(`[TaskPool] 任务完成,耗时: ${endTime - startTime}ms,计算校验值: ${result.toFixed(2)}`);

  // 返回处理后的数据 (此处直接返回原数据用于演示)
  // 注意:默认情况下,返回值会通过序列化拷贝回主线程
  // 如果使用 setTransferList,则不需要拷贝
  return buffer;
}

@Entry
@Component
struct ThreadConcurrencyPage {
  @State isProcessing: boolean = false;
  @State processResult: string = '等待处理...';
  @State progressValue: number = 0;
  
  // 用于模拟 UI 动画的定时器,验证主线程是否卡死
  private animationTimer: number = -1;

  aboutToAppear(): void {
    // 启动一个主线程动画,证明 UI 没卡死
    // 每 50ms 更新一次进度,让进度环转动
    this.animationTimer = setInterval(() => {
      this.progressValue = (this.progressValue + 5) % 100;
    }, 50);
  }

  aboutToDisappear(): void {
    clearInterval(this.animationTimer);
  }

  // -------------------------------------------------------------
  // 2. 触发 TaskPool 任务
  // -------------------------------------------------------------
  async startAsyncTask() {
    if (this.isProcessing) return;
    this.isProcessing = true;
    this.processResult = '正在后台线程全速计算中...';

    try {
      // 模拟一个 10MB 的图片数据
      const imageSize = 1024 * 1024 * 10;
      const mockBuffer = new ArrayBuffer(imageSize);

      // 创建 Task 对象
      // 参数1: @Concurrent 函数
      // 参数2...n: 传递给函数的参数
      const task = new taskpool.Task(heavyImageProcess, mockBuffer, 50000000);
      
      // 【性能优化关键点】:
      // 如果数据很大,强烈建议使用 setTransferList 将 ArrayBuffer 的控制权“转移”给子线程
      // 这样主线程的 mockBuffer 将瞬间变得不可用 (byteLength 为 0),但避免了巨大的拷贝开销
      // 如果开启下面这行注释,传入子线程是零拷贝,但主线程这边的 mockBuffer 就废了
      // task.setTransferList([mockBuffer]); 

      // 执行任务并等待结果
      // execute 返回的是 Promise,不会阻塞当前主线程
      const result = await taskpool.execute(task);
      
      // 类型断言:确保返回的是 ArrayBuffer
      const resultBuffer = result as ArrayBuffer;

      this.processResult = `处理成功!\n数据大小: ${(resultBuffer.byteLength / 1024 / 1024).toFixed(2)} MB`;
      promptAction.showToast({ message: '后台任务执行完毕' });

    } catch (e) {
      console.error(`Task execution failed: ${JSON.stringify(e)}`);
      this.processResult = '处理失败';
    } finally {
      this.isProcessing = false;
    }
  }

  build() {
    Column() {
      Text('TaskPool 多线程并发')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 40, bottom: 20 })

      // 状态展示区
      Column({ space: 20 }) {
        // 一个一直在动的进度条,用于检测 UI 线程是否卡顿
        // 如果主线程被阻塞,这个进度条会停止转动
        Progress({ value: this.progressValue, total: 100, type: ProgressType.Ring })
          .width(80)
          .height(80)
          .color('#0A59F7')
          .style({ strokeWidth: 10 })
          .animation({ duration: 100 })

        Text(this.processResult)
          .fontSize(16)
          .fontColor('#666')
          .textAlign(TextAlign.Center)
          .padding(20)
      }
      .width('90%')
      .padding(30)
      .backgroundColor(Color.White)
      .borderRadius(16)
      .shadow({ radius: 10, color: '#1A000000' })
      .margin({ bottom: 40 })

      // 操作按钮
      Button(this.isProcessing ? '正在计算中...' : '开始耗时计算 (5000万次)')
        .width('80%')
        .height(50)
        .backgroundColor(this.isProcessing ? '#CCCCCC' : '#0A59F7')
        .enabled(!this.isProcessing)
        .onClick(() => {
          this.startAsyncTask();
        })

      Text('原理说明:\n点击按钮后,TaskPool 会在后台线程执行数千万次浮点运算。请观察上方的进度圈,它依然保持流畅转动,说明主线程(UI线程)未被阻塞。')
        .fontSize(12)
        .fontColor('#999')
        .padding(30)
        .lineHeight(20)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F3F5')
  }
}

六、 总结与实战

在鸿蒙 HarmonyOS 6 的开发中,“主线程只做 UI 渲染和轻量逻辑,耗时任务一律扔给后台” 应当成为我们的肌肉记忆。

对于绝大多数场景,TaskPool 是最简单高效的选择,它屏蔽了线程管理的复杂性;而对于需要长时保活的逻辑,Worker 则是不可或缺的补充。同时,我们要善用 ArrayBuffer转移机制 来优化大数据通信的性能。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐