🛑 前言:Java 里的锁,在鸿蒙里不存在

在 Java/Android 中,多线程的本质是:多个线程读写同一块内存(Heap),为了防止数据打架,我们引入了 synchronizedLock

而在 ArkTS 中,采用了 Actor 模型
每一个线程(无论是 UI 主线程,还是后台线程)都是一个独立的 Actor。它们拥有完全独立的内存空间
这意味着:线程 A 修改了全局变量 User.name,线程 B 根本看不到! 线程 B 里的 User.name 还是旧值(或者是复制过去的一个副本)。

所以,别再找 synchronized 了,学会“消息传递”才是鸿蒙并发的正道。


🆚 一、 核心概念:TaskPool vs Worker

鸿蒙提供了两种并发工具,很多新手傻傻分不清楚。

1. TaskPool (任务池) - 推荐首选
  • 定位:轻量级、系统托管、自动扩缩容。
  • Java 映射:类似 ForkJoinPool 或 带有自动回收机制的 CachedThreadPool
  • 特点:你只需要把任务丢进去,系统会自动根据 CPU 负载调度。任务执行完,线程可能会被回收。
  • 适用场景:耗时较短的计算任务(图片压缩、数据过滤)、独立的耗时逻辑。
2. Worker (工作线程) - 重型武器
  • 定位:重量级、常驻内存、手动管理生命周期。
  • Java 映射:类似 new Thread()HandlerThread,带有一个独立的 Looper
  • 特点:它启动慢,占用内存高(每个 Worker 都有独立的 JS 引擎实例),但它启动后会一直活着,等待消息。
  • 适用场景:需要长时间运行的后台服务(如保持 WebSocket 长连接、巨大的文件读写流处理)。

决策流程图 (Mermaid):

是 (Long Running)

否 (One Shot)

注意

优势

我有并发任务

任务是否需要长时间常驻
(如长连接/监听)?

使用 Worker

任务是否超过 3 分钟?

✅ 使用 TaskPool (首选)

最多只能创建 8 个 Worker

自动负载均衡 / 优先级调度


🛠️ 二、 TaskPool 实战:抛弃 Runnable

在 Java 里,你实现 Runnable。在 ArkTS 里,你只需要一个 @Concurrent 装饰器。

错误示范 (Java 思维):

// ❌ 全局变量,想在线程间共享
let progress = 0;

function runTask() {
  // 这里的修改,主线程根本看不见!
  progress = 100; 
}

正确示范 (TaskPool):

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

// 1. 必须使用 @Concurrent 装饰并发函数
@Concurrent
function imageProcessing(buffer: ArrayBuffer): string {
  // 模拟耗时操作
  console.info("TaskPool: 正在处理图片...");
  return "Processed_Result";
}

@Entry
@Component
struct Index {
  @State result: string = "等待处理";

  build() {
    Button("执行任务")
      .onClick(async () => {
        // 2. 准备数据 (注意:数据会被序列化拷贝,或者转移控制权)
        let mockBuffer = new ArrayBuffer(100);
        
        // 3. 创建 Task
        let task = new taskpool.Task(imageProcessing, mockBuffer);
        
        try {
          // 4. 执行并等待结果 (Promise)
          let val = await taskpool.execute(task);
          this.result = val as string;
        } catch (e) {
          console.error("执行失败: " + e);
        }
      })
  }
}


🏭 三、 Worker 实战:建立通信管道

Worker 更像是一个独立的微服务,主线程和 Worker 之间通过 postMessageonmessage 互相喊话。

步骤 1:创建 Worker 脚本 (worker.ts)

import { worker, MessageEvents, ThreadWorkerGlobalScope } from '@kit.ArkTS';

const workerPort: ThreadWorkerGlobalScope = worker.workerPort;

// 监听主线程发来的消息
workerPort.onmessage = (e: MessageEvents) => {
  let data = e.data;
  console.info("Worker 收到: " + data);

  // 处理业务...
  let result = "Worker 处理完成: " + data;

  // 发送回主线程
  workerPort.postMessage(result);
}

步骤 2:主线程调用

import { worker, MessageEvents } from '@kit.ArkTS';

// 创建 Worker (路径要在 build-profile.json5 中配置)
const myWorker = new worker.ThreadWorker("entry/ets/workers/MyWorker.ts");

// 发送消息
myWorker.postMessage("Hello from UI");

// 接收结果
myWorker.onmessage = (e: MessageEvents) => {
  console.info("主线程收到: " + e.data);
}

// 用完记得销毁!
// myWorker.terminate();


💣 四、 避坑指南:那些年 Java 程序员踩过的雷

1. 内存拷贝的代价

Java 线程传对象是传引用(零成本)。
ArkTS 线程传对象是序列化拷贝 (Structured Clone)
如果你传一个 10MB 的图片数据给 TaskPool,系统会把这 10MB 复制一份。这非常耗时!
解决方案:使用 Transferable 对象(如 ArrayBuffer),将内存的所有权**“移交”**给子线程(移交后主线程就不能用了),实现零拷贝。

2. UI 更新的陷阱

子线程(TaskPool/Worker)绝对不能直接访问 UI 组件或 @State 变量。
解决方案:必须把结果 return 回来(TaskPool)或者 postMessage 回来(Worker),在主线程的回调里更新 UI。

3. 单例模式失效

你在主线程写了一个 Singleton 单例。
在 Worker 里调用 Singleton.getInstance(),你会得到一个新的单例对象。
原因:内存隔离。
解决方案:通过构造函数参数传递所需的状态,不要依赖全局单例。


🎯 总结

从 Java 到 ArkTS,并发编程的范式发生了彻底的改变:

  • 从“抢占式 + 锁” 变成了 “协作式 + 消息”
  • TaskPool 是你的日常武器,好用且省心。
  • Worker 是你的重型火炮,非必要不使用。

记住一句话:在鸿蒙里,线程之间是“老死不相往来”的邻居,想办事?请写信(发消息)。

Next Step:
检查你的鸿蒙项目,是不是还在主线程做 JSON 解析?赶紧封装一个 @Concurrent 函数,丢进 TaskPool 里跑一下,你会发现界面流畅度提升了一个档次!

Logo

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

更多推荐