HarmonyOS Next 并发 taskpool 和 worker

简介: HarmonyOS Next 提供了 TaskPool 和 Worker 两种并发能力,基于 Actor 并发模型实现。TaskPool 是 Worker 的封装,支持参数直接传递、返回数据接收、任务优先级设置及取消功能,适合大多数场景;Worker 则适用于超长任务或需手动管理线程生命周期的场景。两者通过消息通信完成跨线程数据交换,支持普通对象拷贝、ArrayBuffer 拷贝/转移、SharedArrayBuffer 共享及 Sendable 引用传递等方式。实际开发中,TaskPool 更简化任务调度,而 Worker 更灵活,可根据任务类型(耗时、长时、常驻)选择合适方案。

HarmonyOS Next 并发 taskpool 和 worker

总览

image-20241110140848578

介绍

并发,指的是同一时间内,多段代码同时执行。在 ArkTs 编程中,并发分为异步并发和多线程并发。

异步并发

异步并发并不是真正的并发,比如在单核设备中,同时执行多端代码其实是通过 CPU 快速调度来实现的。比如一个司机,它在同一时间只

能开一辆车。做不到同时开两辆车。如果举一个极端的例子。这位司机可以看起来像在开两辆车。

我们把开车分成两个步骤:

  1. 上车
  2. 开动

如果我们把司机上车的时间极限缩小,比如为 0.00001 秒中,那么这个司机就可以做到同时开两辆车

开小米 su7

  1. 上车(0.00001 秒)
  2. 开车

马上下车,跑到另外一辆车旁边,然后

开问界 M7

  1. 上车(0.00001 秒)
  2. 开车

可以看到,只要切换任务的速度够快,就能理解成同时在开两辆车。这个就是我们的忙碌的 CPU 要做的事情了。

我们可以将 setTimeout 和 Promise/Async 都看成是 异步并发的技术就行。

多线程并发

多线程并发的模型常见的分成基于内存共享的并发模型和基于消息通信的并发模型。

Actor 并发模型作为基于消息通信并发模型的典型代表,不需要开发者去面对锁带来的一系列复杂偶发的问题,同时并发度也相对较高,

因此得到了广泛的支持和使用。

当前 ArkTS 提供了TaskPoolWorker两种并发能力,TaskPoolWorker都基于Actor并发模型实现。 TaskPoolWorker

TaskPool 其实是基于 Worker 做到的一层封装。

TaskPool 和 Worker 的实现特点对比

主要是使用方法上的区别

  1. TaskPool 可以直接传递参数、Worker 需要自行封装参数
  2. TaskPool 可以直接接收返回数据,Worker 通过 onmessage 接收
  3. TaskPool 任务池个数上限自动管理,Worker 最多 64 个线程,具体看内存
  4. TaskPool 任务执行时长 同步代码上限 3 分钟,异步代码无限。Worker 无限
  5. TaskPool 可以设置设置任务的优先级,Workder 不支持
  6. TaskPool 支持取消任务,Worker 不支持

image-20241110105619585

TaskPool 和 Worker 的适用场景对比

TaskPool 和 Worker 均支持多线程并发能力。由于 TaskPool 的工作线程会绑定系统的调度优先级,并且支持负载均衡(自动扩缩容),而

Worker 需要开发者自行创建,存在创建耗时以及不支持设置调度优先级,故在性能方面使用 TaskPool 会优于 Worker,因此大多数场景推

荐使用 TaskPool

TaskPool 偏向独立任务维度,该任务在线程中执行,无需关注线程的生命周期,超长任务(大于 3 分钟且非长时任务)会被系统自动回

收;而 Worker 偏向线程的维度,支持长时间占据线程执行,需要主动管理线程生命周期。

常见的一些开发场景及适用具体说明如下:

  • 运行时间超过 3 分钟(不包含 Promise 和 async/await 异步调用的耗时,例如网络下载、文件读写等 I/O 任务的耗时)的任务。例如后台

    进行 1 小时的预测算法训练等 CPU 密集型任务,需要使用 Worker。

  • 有关联的一系列同步任务。例如在一些需要创建、使用句柄的场景中,句柄创建每次都是不同的,该句柄需永久保存,保证使用该句

    柄进行操作,需要使用 Worker。

  • 需要设置优先级的任务。例如图库直方图绘制场景,后台计算的直方图数据会用于前台界面的显示,影响用户体验,需要高优先级处理,需要使用 TaskPool

  • 需要频繁取消的任务。例如图库大图浏览场景,为提升体验,会同时缓存当前图片左右侧各 2 张图片,往一侧滑动跳到下一张图片

    时,要取消另一侧的一个缓存任务,需要使用 TaskPool。

  • 大量或者调度点较分散的任务。例如大型应用的多个模块包含多个耗时任务,不方便使用 Worker 去做负载管理,推荐采用

    TaskPool。

线程间通信对象

线程间通信指的是并发多线程间存在的数据交换行为。由于 ArkTS 语言兼容 TS/JS,其运行时的实现与其它所有的 JS 引擎一样,都是基于

Actor 内存隔离的并发模型提供并发能力。

普通对象

普通对象跨线程时通过拷贝形式传递

ArrayBuffer 对象

ArrayBuffer 内部包含一块 Native 内存。其 JS 对象壳与普通对象一样,需要经过序列化与反序列化拷贝传递,但是 Native 内存有两种传输方

式:拷贝和转移。

SharedArrayBuffer 对象

SharedArrayBuffer 内部包含一块 Native 内存,支持跨并发实例间共享,但是访问及修改需要采用Atomics类,防止数据竞争

Transferable

Transferable 对象(也称为 NativeBinding 对象)指的是一个 JS 对象,绑定了一个 C++对象,且主体功能由 C++提供

可以实现共享和转移模式

Sendable 对象

在传统 JS 引擎上,对象的并发通信开销的优化方式只有一种,就是把实现下沉到 Native 侧,通过Transferable 对象的转移或共享方式降低并发通信开销。而开发者仍然还有大量对象并发通信的诉求,这个问题在业界的 JS 引擎实现上并没有得到解决。

ArkTS 提供了 Sendable 对象类型,在并发通信时支持通过引用传递来解决上述问题。

Sendable 对象为可共享的,其跨线程前后指向同一个 JS 对象,如果其包含了 JS 或者 Native 内容,均可以直接共享,如果底层是 Native 实现

的,则需要考虑线程安全性。

多线程并发场景

针对常见的业务场景,主要可以对应分为三种并发任务:耗时任务长时任务常驻任务

耗时任务并发场景简介

耗时任务指的是需要长时间执行的任务,如果在主线程执行可能导致应用卡顿掉帧、响应慢等问题。典型的耗时任务有 CPU 密集型任务、I/O 密集型任务以及同步任务。

对应耗时任务,常见的业务场景分类如下所示:

常见业务场景 具体业务描述 CPU 密集型 I/O 密集型 同步任务
图片/视频编解码 将图片或视频进行编解码再展示。 ×
压缩/解压缩 对本地压缩包进行解压操作或者对本地文件进行压缩操作。 ×
JSON 解析 对 JSON 字符串的序列化和反序列化操作。 × ×
模型运算 对数据进行模型运算分析等。 × ×
网络下载 密集网络请求下载资源、图片、文件等。 × ×
数据库操作 将聊天记录、页面布局信息、音乐列表信息等保存到数据库,或者应用二次启动时,读取数据库展示相关信息。 × ×

长时任务并发场景简介

在应用业务实现过程中,对于需要较长时间不定时运行的任务,称为长时任务。长时任务如果放在主线程中执行会阻塞主线程的 UI 业务,出现卡顿丢帧等影响用户体验的问题。因此通常需要将这个独立的长时任务放到单独的子线程中执行。

典型的长时任务场景如下所示:

常见业务场景 具体业务描述
定期采集传感器数据 周期性采集一些传感器信息(例如位置信息、速度传感器等),应用运行阶段长时间不间断运行。
监听 Socket 端口信息 长时间监听 Socket 数据,不定时需要响应处理。

上述业务场景均为独立的长时任务,任务执行周期长,跟外部交互简单,分发到后台线程后,需要不定期响应,以获取结果。这些类型的

任务使用 TaskPool 可以简化开发工作量,避免管理复杂的生命周期,避免线程泛滥,开发者只需要将上述独立的长时任务放入 TaskPool

队列,再等待结果即可。

常驻任务并发场景

在应用业务实现过程中,对于一些长耗时(大于 3min)且并发量不大的常驻任务场景,使用 Worker 在后台线程中运行这些耗时逻辑,避免阻塞主线程而导致出现丢帧卡顿等影响用户体验性的问题 。

常驻任务是指相比于短时任务,时间更长的任务,可能跟主线程生命周期一致。相比于长时任务,常驻任务更倾向于跟线程绑定的任务,单次运行时间更长(比如超过 3 分钟)。

对应常驻任务,较为常见的业务场景如下:

常见业务场景 具体业务描述
游戏中台场景 启动子线程作为游戏业务的主逻辑线程,UI 线程只负责渲染。
长耗时任务场景 后台长时间的模型预测任务、或者硬件测试等

TaskPool 使用

TaskPool 基本使用 1

TaskPool 最简单的用法如下

其中需要注意的是执行任务的函数必须使用 @Concurrent 来修饰

taskpool.execute(函数, 参数);
AI 代码解读

image-20241110123246585

import {
    taskpool } from '@kit.ArkTS'

// 任务具体逻辑
@Concurrent
function func1(n: number) {
   
  return 1 + n
}

@Entry
@Component
struct Index {
   
  @State
  num: number = 0
  fn1 = async () => {
   
    // 执行任务
    const res = await taskpool.execute(func1, this.num)
    this.num = Number(res)
  }

  build() {
   
    Column() {
   
      Button("TaskPool基本使用 " + this.num)
        .onClick(this.fn1)
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}
AI 代码解读

TaskPool 基本使用 2

TaskPool 的基本使用还可以指定任务的优先级 如:

  taskpool.execute(task, 优先级)
AI 代码解读

image-20241110124508588

import {
    taskpool } from '@kit.ArkTS'

// 任务具体逻辑
@Concurrent
function func1(n: number) {
   
  console.log(`任务 ${
     n}`)
}

@Entry
@Component
struct Index {
   
  @State
  num: number = 0
  fn1 = async () => {
   
    // 同时创建和执行10个任务  指定第10个任务是最高优先级
    for (let index = 1; index <= 10; index++) {
   
      // 创建任务,传入要执行的函数和该函数的形参
      const task = new taskpool.Task(func1, index)
      if (index !== 10) {
   
        // 执行任务,并且制定优先级 taskpool.Priority.LOW
        taskpool.execute(task, taskpool.Priority.LOW)
      } else {
   
        taskpool.execute(task, taskpool.Priority.HIGH)
      }
    }
  }

  build() {
   
    Column() {
   
      Button("TaskPool基本使用 " + this.num)
        .onClick(this.fn1)
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}
AI 代码解读

TaskPool 基本使用 3

TaskPool 还可以支持传入一些任务组,类似 Promise.All, 可以等待全部的耗时任务结束。

import {
    taskpool } from '@kit.ArkTS'

// 任务具体逻辑
@Concurrent
function func1(n: number) {
   
  const promise = new Promise<number>(resolve => {
   
    resolve(n)
    setTimeout(() => {
   
    }, 1000 + n * 100)
  })
  return promise
}

@Entry
@Component
struct Index {
   
  fn1 = async () => {
   
    // 创建任务组
    const taskGroup = new taskpool.TaskGroup()
    for (let index = 1; index <= 10; index++) {
   
      // 创建任务,传入要执行的函数和该函数的形参
      const task = new taskpool.Task(func1, index)
      // 添加到任务组中
      taskGroup.addTask(task)
    }
    // 执行任务组
    const res = await taskpool.execute(taskGroup)
    console.log("res", JSON.stringify(res))


  }

  build() {
   
    Column() {
   
      Button("TaskPool基本使用 执行一组任务 ")
        .onClick(this.fn1)
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}
AI 代码解读

TaskPool 和主线程通信

TaskPool 可以直接通过返回结果,将数据传递回来。但是如果,是持续监听的任务,那么此时可以使用 sendDataonReceiveData 和主线程进行通信

image-20241110131624375

import {
    taskpool } from '@kit.ArkTS'

// 任务具体逻辑
@Concurrent
function func1(n: number) {
   
  // 假设这里是持续发送的数据
  taskpool.Task.sendData((new Date).toLocaleString())
  return 100
}

@Entry
@Component
struct Index {
   
  fn1 = async () => {
   
    const task = new taskpool.Task(func1, 100)

    //   主线程监听到的数据
    task.onReceiveData((result: string) => {
   
      console.log("主线程监听到的数据", result)
    })

    //   直接得到func1的结果
    const res = await taskpool.execute(task)
    console.log("直接返回的结果", res)
  }

  build() {
   
    Column() {
   
      Button("TaskPool基本使用 和主线程通信 ")
        .onClick(this.fn1)
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}
AI 代码解读

TaskPool 使用限制

TaskPool 使用过程中,不能直接使用 TaskPool 所在同一文件中的变量、函数或者类,否则会报错误

image-20241110132305457

在使用 @Concurrent 修饰的函数中,只能使用导入的变量和局部变量。<ArkTSCheck>

Only imported variables and local variables can be used in @Concurrent decorated functions. <ArkTSCheck>
AI 代码解读

解决方案是 导出使用

image-20241110132421889

Worker 使用

Worker 主要作用是为应用程序提供一个多线程的运行环境,可满足应用程序在执行过程中与主线程分离,在后台线程中运行一个脚本进

行耗时操作,极大避免类似于计算密集型或高延迟的任务阻塞主线程的运行

workder 主要通过

  1. onmessage 监听数据
  2. postMessage 发送数据

image-20241110135833698

1. 新建 workder 文件

在你的模块上鼠标右键新建 worker 模块即可

image-20241110135437606

entry/src/main/ets/workers/Worker.ets

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

const workerPort: ThreadWorkerGlobalScope = worker.workerPort;

workerPort.onmessage = (e: MessageEvents) => {
   
  // 接收数据
  const data = e.data as Record<string, number>;
  // 发送给主线程
  console.log("子线程监听到了信息,并且发送信息给主线程");
  setTimeout(() => {
   
    workerPort.postMessage(data.a + data.b);
  }, 1000);
};

workerPort.onmessageerror = (e: MessageEvents) => {
   };

workerPort.onerror = (e: ErrorEvent) => {
   };
AI 代码解读

2. 主线程使用 woker 进行通信

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

// 1 声明workder
let workerStage1: worker.ThreadWorker | null

@Entry
@Component
struct Index {
   
  build() {
   
    Column() {
   
      Button("创建")
        .onClick(() => {
   
          // 2. 创建worker
          workerStage1 = new worker.ThreadWorker('entry/ets/workers/Worker.ets');
        })
      Button("监听数据")
        .onClick(() => {
   
          // 3 监听workder接收到的信息
          workerStage1!.onmessage = (e: MessageEvents) => {
   
            console.log("总和", e.data)
          }
        }
        )
      Button("发送数据")
        .onClick(() => {
   
          // 4 向 workder发送信息
          workerStage1!.postMessage({
    a: 10, b: 20 })
        })

    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}
AI 代码解读

总结

image-20241110140840938

目录
打赏
0
12
12
0
133
分享
相关文章
|
2天前
|
HarmonyOS NEXT - 通用属性
这段代码展示了组件的通用属性设置方法,涵盖尺寸、位置、布局约束、Flex布局、边框、背景及图像效果等多方面。通过TypeScript实现,提供了灵活的样式配置选项。例如,`.size()` 和 `.width()` 用于调整组件尺寸;`.align()` 和 `.position()` 控制对齐与定位;`.border()` 系列方法定义边框样式;`.backgroundColor()` 和 `.backgroundImage()` 设置背景;图像效果如模糊、阴影、灰度等可通过相应方法实现。
53 0
|
2天前
|
HarmonyOS NEXT 通用事件
通用事件涵盖事件分发、触屏事件、键鼠事件、焦点事件和拖拽事件等多种类型。事件分发将用户操作生成的触控事件分配至各组件;触屏事件包括Touch与Mouse类事件;键鼠事件处理外接设备输入;焦点事件涉及焦点获取、失去及焦点链管理;拖拽事件支持数据在组件间传递。 代码示例展示了如何通过`onClick`实现点击计数、`onFocus`与`onBlur`处理焦点事件,以及通过`bindPopup`展示气泡弹窗提示。此外,还提供了输入框的焦点状态管理与验证逻辑,确保用户体验流畅且交互清晰。
53 0
|
2天前
HarmonyOS NEXT - @Prop和@Link
本示例介绍了`@Prop`和`@Link`装饰器在父子组件间的数据同步机制。`@Prop`实现单向数据绑定,子组件可修改本地值,但不会同步回父组件;父组件数据更新时会覆盖子组件的本地更改。`@Link`实现双向绑定,子组件与父组件数据共享且相互影响。 **限制条件:** - `@Prop`变量深拷贝时可能丢失复杂类型。 - `@Link`不可用于`@Entry`组件,禁止本地初始化,类型需与数据源一致。 **支持类型:** - `@Prop`支持基础类型、对象、数组、`Date`及联合类型,不支持`any`。 - 数据源与`@Prop`类型需匹配,包括简单类型、数组项及对象属性。
57 41
HarmonyOS NEXT - RelationalStore关系型数据库
关系型数据库对应用提供通用的操作接口,底层使用SQLite作为持久化存储引擎,支持SQLite具有的数据库特性,包括但不限于事务、索引、视图、触发器、外键、参数化查询和预编译SQL语句。
55 27
|
2天前
|
HarmonyOS NEXT - @Provide和@Consume
@Provide和@Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递,@Provide和@Consume摆脱参数传递机制的束缚,实现跨层级传递。
55 21
HarmonyOS NEXT - Preferences用户首选项
- 用户首选项为应用提供Key-Value键值型的数据处理能力,支持应用持久化轻量级数据,并对其修改和查询。 - Preferences会将该数据缓存在内存中,当用户读取的时候,能够快速从内存中获取数据。Preferences会随着存放的数据量越多而导致应用占用的内存越大,因此,Preferences不适合存放过多的数据。
56 19
|
3天前
|
HarmonyOS NEXT - ArkUI: Button组件
Button是用于响应用户点击操作的按钮组件,支持胶囊型、圆形和普通三种样式。可通过`type`属性设置样式(默认为胶囊型),并使用`stateEffect`控制按压态效果(默认开启)。Button可包含子组件,实现复杂功能按钮;支持自定义文本样式、背景色及边框弧度等样式。示例代码展示了不同类型按钮的创建、子组件嵌套及点击事件处理方法。
51 18
|
2天前
HarmonyOS NEXT - @State状态变量
ArkUI 是一种声明式 UI 框架,通过状态驱动 UI 更新。@State 装饰的变量用于管理组件内部状态,具有以下特点:私有性(仅组件内访问)、必须初始化、生命周期与组件一致。它支持单向(与 @Prop)和双向(与 @Link、@ObjectLink)数据同步。状态改变时,绑定的 UI 会自动刷新。注意:@State 不支持 Function 类型,不能在 build 中修改状态变量。代码示例中展示了通过 @State 管理按钮点击计数的状态更新机制。
53 16
|
2天前
|
HarmonyOS NEXT - AlertDialog警告弹窗
`AlertDialog` 是一个用于显示警告弹窗的组件,支持自定义文本内容与回调操作。通过 `AlertDialog.show()` 方法,传入配置参数即可展示弹窗。主要功能包括设置标题、内容、按钮、模态状态、对齐方式、过渡动画等。示例代码展示了如何创建一个包含“取消”和“删除”按钮的弹窗,并在点击按钮后执行相应业务逻辑,同时更新页面消息提示。适用于需要用户确认或交互的场景。
51 14
HarmonyOS NEXT - ArkUI: TextInput组件
TextInput组件是用于输入单行文本的核心组件,广泛应用于登录账号、密码输入及消息发送等场景。支持通过`placeholder`设置提示文字、`text`初始化文本内容以及`controller`控制输入行为。提供多种输入类型(如普通、密码、邮箱、数字)通过`.type()`方法设置,并可通过`.onChange()`监听文本变化。 示例代码展示了基本用法,包括账号、密码和手机号的输入框实现。此外,组件支持字符计数功能,通过`.maxLength()`限制最大字符数、`.showCounter()`显示计数器,并可自定义阈值和高亮边框效果,满足复杂输入场景需求。
51 15