HarmonyOS:多线程并发-Worker

Worker主要作用是为应用程序提供一个多线程的运行环境,可满足应用程序在执行过程中与宿主线程分离,在后台线程中运行一个脚本进行耗时操作,极大避免类似于计算密集型或高延迟的任务阻塞宿主线程的运行。具体接口信息及使用方法详情请见Worker。

一、Worker运作机制

Worker运作机制示意图

创建Worker的线程称为宿主线程(不一定是主线程,工作线程也支持创建Worker子线程),Worker自身的线程称为Worker子线程(或Actor线程、工作线程)。每个Worker子线程与宿主线程拥有独立的实例,包含基础设施、对象、代码段等,因此每个Worker启动存在一定的内存开销,需要限制Worker的子线程数量。Worker子线程和宿主线程之间的通信是基于消息传递的,Worker通过序列化机制与宿主线程之间相互通信,完成命令及数据交互。

二、Worker注意事项

  • 创建Worker时,有手动和自动两种创建方式,手动创建Worker线程目录及文件时,还需同步进行相关配置,详情请参考创建Worker的注意事项。
  • 使用Worker能力时,构造函数中传入的Worker线程文件的路径在不同版本有不同的规则,详情请参见文件路径注意事项
  • Worker创建后需要手动管理生命周期,且最多同时运行的Worker子线程数量为64个,详情请参见生命周期注意事项
  • 由于不同线程中上下文对象是不同的,因此Worker线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用。
  • 序列化传输的数据量大小限制为16MB。
  • 使用Worker模块时,需要在宿主线程中注册onerror接口,否则当Worker线程出现异常时会发生jscrash问题。
  • 不支持跨HAP使用Worker线程文件。
  • 引用HAR/HSP前,需要先配置对HAR/HSP的依赖,详见引用共享包
  • 不支持在Worker工作线程中使用AppStorage

三、创建Worker的注意事项

Worker线程文件需要放在"{moduleName}/src/main/ets/"目录层级之下,否则不会被打包到应用中。有手动和自动两种创建Worker线程目录及文件的方式。

  • 手动创建:开发者手动创建相关目录及文件,此时需要配置build-profile.json5的相关字段信息,Worker线程文件才能确保被打包到应用中。
    Stage模型:
复制代码
"buildOption": {
  "sourceOption": {
    "workers": [
      "./src/main/ets/workers/worker.ets"
    ]
  }
}

FA模型:

复制代码
"buildOption": {
  "sourceOption": {
    "workers": [
      "./src/main/ets/MainAbility/workers/worker.ets"
    ]
  }
}
  • 自动创建:DevEco Studio支持一键生成Worker,在对应的{moduleName}目录下任意位置,点击鼠标右键 > New > Worker,即可自动生成Worker的模板文件及配置信息,无需再手动在build-profile.json5中进行相关配置。

四、文件路径注意事项

当使用Worker模块具体功能时,均需先构造Worker实例对象,其构造函数与API版本相关,且构造函数需要传入Worker线程文件的路径(scriptURL)。

复制代码
// 导入模块
import { worker } from '@kit.ArkTS';

// API 9及之后版本使用:
const worker1: worker.ThreadWorker = new worker.ThreadWorker('entry/ets/workers/worker.ets');
// API 8及之前版本使用:
const worker2: worker.Worker = new worker.Worker('entry/ets/workers/worker.ets');
4.1 Stage模型下的文件路径规则

构造函数中的scriptURL要求如下:

  • scriptURL的组成包含 {moduleName}/ets 和相对路径 relativePath。
  • relativePath是Worker线程文件相对于"{moduleName}/src/main/ets/"目录的相对路径。
4.1.1 加载Ability中Worker线程文件场景

加载Ability中的worker线程文件,加载路径规则:{moduleName}/ets/{relativePath}。

复制代码
import { worker } from '@kit.ArkTS';

// worker线程文件所在路径:"entry/src/main/ets/workers/worker.ets"
const workerStage1: worker.ThreadWorker = new worker.ThreadWorker('entry/ets/workers/worker.ets');

// worker线程文件所在路径:"phone/src/main/ets/ThreadFile/workers/worker.ets"
const workerStage2: worker.ThreadWorker = new worker.ThreadWorker('phone/ets/ThreadFile/workers/worker.ets');
4.1.2 加载HSP中Worker线程文件场景

加载HSP中worker线程文件,加载路径规则:{moduleName}/ets/{relativePath}。

复制代码
import { worker } from '@kit.ArkTS';

// worker线程文件所在路径: "hsp/src/main/ets/workers/worker.ets"
const workerStage3: worker.ThreadWorker = new worker.ThreadWorker('hsp/ets/workers/worker.ets');
4.1.3 加载HAR中Worker线程文件场景

加载HAR中worker线程文件存在以下两种情况:

  • @标识路径加载形式:所有种类的模块加载本地HAR中的Worker线程文件,加载路径规则:@{moduleName}/ets/{relativePath}。
  • 相对路径加载形式:本地HAR加载该包内的Worker线程文件,加载路径规则:创建Worker对象所在文件与Worker线程文件的相对路径。
    说明

当开启useNormalizedOHMUrl(即将工程目录中与entry同级别的应用级build-profile.json5文件中strictMode属性的useNormalizedOHMUrl字段配置为true)或HAR包会被打包成三方包使用时,则HAR包中使用Worker仅支持通过相对路径的加载形式创建。

复制代码
import { worker } from '@kit.ArkTS';

// @标识路径加载形式:
// worker线程文件所在路径: "har/src/main/ets/workers/worker.ets"
const workerStage4: worker.ThreadWorker = new worker.ThreadWorker('@har/ets/workers/worker.ets');

// 相对路径加载形式:
// worker线程文件所在路径: "har/src/main/ets/workers/worker.ets"
// 创建Worker对象的文件所在路径:"har/src/main/ets/components/mainpage/MainPage.ets"
const workerStage5: worker.ThreadWorker = new worker.ThreadWorker('../../workers/worker.ets');
4.2 FA模型下的文件路径规则

构造函数中的scriptURL为:Worker线程文件与"{moduleName}/src/main/ets/MainAbility"的相对路径。

复制代码
import { worker } from '@kit.ArkTS';

// 主要说明以下三种场景:

// 场景1: Worker线程文件所在路径:"{moduleName}/src/main/ets/MainAbility/workers/worker.ets"
const workerFA1: worker.ThreadWorker = new worker.ThreadWorker("workers/worker.ets", {name:"first worker in FA model"});

// 场景2: Worker线程文件所在路径:"{moduleName}/src/main/ets/workers/worker.ets"
const workerFA2: worker.ThreadWorker = new worker.ThreadWorker("../workers/worker.ets");

// 场景3: Worker线程文件所在路径:"{moduleName}/src/main/ets/MainAbility/ThreadFile/workers/worker.ets"
const workerFA3: worker.ThreadWorker = new worker.ThreadWorker("ThreadFile/workers/worker.ets");

五、生命周期注意事项

  • Worker的创建和销毁耗费性能,建议开发者合理管理已创建的Worker并重复使用。Worker空闲时也会一直运行,因此当不需要Worker时,可以调用terminate()接口或close()方法主动销毁Worker。若Worker处于已销毁或正在销毁等非运行状态时,调用其功能接口,会抛出相应的错误。
  • Worker的数量由内存管理策略决定,设定的内存阈值为1.5GB和设备物理内存的60%中的较小者。在内存允许的情况下,系统最多可以同时运行64个Worker。如果尝试创建的Worker数量超出这一上限,系统将抛出错误:"Worker initialization failure, the number of workers exceeds the maximum."。实际运行的Worker数量会根据当前内存使用情况动态调整。一旦所有Worker和主线程的累积内存占用超过了设定的阈值,系统将触发内存溢出(OOM)错误,导致应用程序崩溃。

六、跨har包加载Worker

  1. 创建har详情参考开发静态共享包
  2. 在har中创建Worker线程文件相关内容。
复制代码
// worker.ets
workerPort.onmessage = (e: MessageEvents) => {
  console.info("worker thread receive message: ", e.data);
  workerPort.postMessage('worker thread post message to main thread');
}
  1. 在entry模块的oh-package.json5文件中配置har包的依赖。
复制代码
// 在entry模块配置har包的依赖
{
  "name": "entry",
  "version": "1.0.0",
  "description": "Please describe the basic information.",
  "main": "",
  "author": "",
  "license": "",
  "dependencies": {
    "har": "file:../har"
  }
}
  1. 在entry模块中加载har包中的Worker线程文件。
复制代码
// Index.ets
import { worker } from '@kit.ArkTS';

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('HelloWorld')
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          // 通过@标识路径加载形式,加载har中Worker线程文件
          let workerInstance = new worker.ThreadWorker('@har/ets/workers/worker.ets');
          workerInstance.onmessage = () => {
            console.info('main thread onmessage');
          };
          workerInstance.postMessage('hello world');
        })
    }
    .height('100%')
    .width('100%')
  }
}

七、多级Worker生命周期管理

由于支持创建多级Worker(即通过父Worker创建子Worker的机制形成层级线程关系),且Worker线程生命周期由用户自行管理,因此需要注意多级Worker生命周期的正确管理。若用户销毁父Worker时未能结束其子Worker的运行,会产生不可预期的结果。建议用户确保子Worker的生命周期始终在父Worker生命周期范围内,并在销毁父Worker前先销毁所有子Worker。

7.1 推荐使用示例
复制代码
// 在主线程中创建Worker线程(父Worker),在worker线程中再次创建Worker线程(子Worker)
// main thread
import { worker, MessageEvents, ErrorEvent } from '@kit.ArkTS';

// 主线程中创建父worker对象
const parentworker = new worker.ThreadWorker("entry/ets/workers/parentworker.ets");

parentworker.onmessage = (e: MessageEvents) => {
  console.info("主线程收到父worker线程信息 " + e.data);
}

parentworker.onexit = () => {
  console.info("父worker退出");
}

parentworker.onerror = (err: ErrorEvent) => {
  console.info("主线程接收到父worker报错 " + err);
}

parentworker.postMessage("主线程发送消息给父worker-推荐示例");

// parentworker.ets
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';

// 创建父Worker线程中与主线程通信的对象
const workerPort: ThreadWorkerGlobalScope = worker.workerPort;

workerPort.onmessage = (e : MessageEvents) => {
  if (e.data == "主线程发送消息给父worker-推荐示例") {
    let childworker = new worker.ThreadWorker("entry/ets/workers/childworker.ets");

    childworker.onmessage = (e: MessageEvents) => {
      console.info("父Worker收到子Worker的信息 " + e.data);
      if (e.data == "子Worker向父Worker发送信息") {
        workerPort.postMessage("父Worker向主线程发送信息");
      }
    }

    childworker.onexit = () => {
      console.info("子Worker退出");
      // 子Worker退出后再销毁父Worker
      workerPort.close();
    }

    childworker.onerror = (err: ErrorEvent) => {
      console.info("子Worker发生报错 " + err);
    }

    childworker.postMessage("父Worker向子Worker发送信息-推荐示例");
  }
}

// childworker.ets
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';

// 创建子Worker线程中与父Worker线程通信的对象
const workerPort: ThreadWorkerGlobalScope = worker.workerPort;

workerPort.onmessage = (e: MessageEvents) => {
  if (e.data == "父Worker向子Worker发送信息-推荐示例") {
    // 子Worker线程业务逻辑...
    console.info("业务执行结束,然后子Worker销毁");
    workerPort.close();
  }
}

执行结果

7.2 不推荐使用示例

不建议父Worker主动销毁后,子Worker仍向父Worker发送消息。

复制代码
// main thread
import { worker, MessageEvents, ErrorEvent } from '@kit.ArkTS';

const parentworker = new worker.ThreadWorker("entry/ets/workers/parentworker.ets");

parentworker.onmessage = (e: MessageEvents) => {
  console.info("主线程收到父Worker信息" + e.data);
}

parentworker.onexit = () => {
  console.info("父Worker退出");
}

parentworker.onerror = (err: ErrorEvent) => {
  console.info("主线程接收到父Worker报错 " + err);
}

parentworker.postMessage("主线程发送消息给父Worker");

// parentworker.ets
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';

const workerPort: ThreadWorkerGlobalScope = worker.workerPort;

workerPort.onmessage = (e : MessageEvents) => {
  console.info("父Worker收到主线程的信息 " + e.data);

  let childworker = new worker.ThreadWorker("entry/ets/workers/childworker.ets")

  childworker.onmessage = (e: MessageEvents) => {
    console.info("父Worker收到子Worker的信息 " + e.data);
  }

  childworker.onexit = () => {
    console.info("子Worker退出");
    workerPort.postMessage("父Worker向主线程发送信息");
  }

  childworker.onerror = (err: ErrorEvent) => {
    console.info("子Worker发生报错 " + err);
  }

  childworker.postMessage("父Worker向子Worker发送信息");

  // 创建子Worker后,销毁父Worker
  workerPort.close();
}

不建议在明确父Worker发起销毁操作的同步调用前后仍在父Worker线程创建子Worker。不建议在不确定父Worker是否发起销毁操作的情况下,仍在父Worker线程创建子Worker,即创建子Worker线程成功之前需保证父Worker线程始终处于存活状态。

复制代码
// main thread
import { worker, MessageEvents, ErrorEvent } from '@kit.ArkTS';

const parentworker = new worker.ThreadWorker("entry/ets/workers/parentworker.ets");

parentworker.onmessage = (e: MessageEvents) => {
  console.info("主线程收到父Worker信息" + e.data);
}

parentworker.onexit = () => {
  console.info("父Worker退出");
}

parentworker.onerror = (err: ErrorEvent) => {
  console.info("主线程接收到父Worker报错 " + err);
}

parentworker.postMessage("主线程发送消息给父Worker");

// parentworker.ets
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';

const workerPort: ThreadWorkerGlobalScope = worker.workerPort;

workerPort.onmessage = (e : MessageEvents) => {
  console.info("父Worker收到主线程的信息 " + e.data);

  // 父Worker销毁后创建子Worker,行为不可预期
  workerPort.close();
  let childworker = new worker.ThreadWorker("entry/ets/workers/childworker.ets");

  // 子Worker线程未确认创建成功前销毁父Worker,行为不可预期
  // let childworker = new worker.ThreadWorker("entry/ets/workers/childworker.ets");
  // workerPort.close();

  childworker.onmessage = (e: MessageEvents) => {
    console.info("父Worker收到子Worker的信息 " + e.data);
  }

  childworker.onexit = () => {
    console.info("子Worker退出");
    workerPort.postMessage("父Worker向主线程发送信息");
  }

  childworker.onerror = (err: ErrorEvent) => {
    console.info("子Worker发生报错 " + err);
  }

  childworker.postMessage("父Worker向子Worker发送信息");
}

// childworker.ets
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';

const workerPort: ThreadWorkerGlobalScope = worker.workerPort;

workerPort.onmessage = (e: MessageEvents) => {
  console.info("子Worker收到信息 " + e.data);
}

八、ThreadWorker

使用以下方法前,均需先构造ThreadWorker实例,ThreadWorker类继承WorkerEventTarget

8.1 constructor

constructor(scriptURL: string, options?: WorkerOptions)

ThreadWorker构造函数。

  • 元服务API:从API version 11 开始,该接口支持在元服务中使用。
  • 系统能力: SystemCapability.Utils.Lang

参数:

参数名 类型 必填 说明
script URL string Worker线程文件的路径。路径规则详细参考文件路径注意事项。
options WorkerOptions Worker构造的选项。

示例:

此处以在Stage模型中Ability加载Worker文件为例,使用Library加载Worker线程文件的场景参考文件路径注意事项。

复制代码
import { worker } from '@kit.ArkTS';

// 主要说明以下两种场景:

// 场景1: worker文件所在路径:"entry/src/main/ets/workers/worker.ets"
const workerStageModel01 = new worker.ThreadWorker('entry/ets/workers/worker.ets', {name:"first worker in Stage model"});

// 场景2: worker文件所在路径:"phone/src/main/ets/ThreadFile/workers/worker.ets"
const workerStageModel02 = new worker.ThreadWorker('phone/ets/ThreadFile/workers/worker.ets');

九、postMessage

postMessage(message: Object, transfer: ArrayBuffer[]): void

宿主线程通过转移对象所有权的方式向Worker线程发送消息。

系统能力 : SystemCapability.Utils.Lang

元服务API:从API version 11 开始,该接口支持在元服务中使用。

参数:

参数名 类型 必填 说明
message Object 发送至Worker的数据,该数据对象必须是可序列化,序列化支持类型见其他说明。
transfer ArrayBuffer[] 表示可转移的ArrayBuffer实例对象数组,该数组中对象的所有权会被转移到Worker线程,在宿主线程中将会变为不可用,仅在Worker线程中可用,数组不可传入null。

示例:

Worker.ets代码

复制代码
// Worker.ets
import { worker, MessageEvents, ErrorEvent } from '@kit.ArkTS';

// 创建worker线程中与宿主线程通信的对象
const workerPort = worker.workerPort

// worker线程接收宿主线程信息
workerPort.onmessage = (e: MessageEvents): void => {
  // data:宿主线程发送的信息
  let data: number = e.data;
  // 往收到的buffer里写入数据
  const view = new Int8Array(data).fill(3);
  // worker线程向宿主线程发送信息
  workerPort.postMessage(view);
}

// worker线程发生error的回调
workerPort.onerror = (err: ErrorEvent) => {
  console.log("worker.ets onerror" + err.message);
}

Index.ets代码

复制代码
// Index.ets
import { worker, MessageEvents, ErrorEvent } from '@kit.ArkTS';

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            // 宿主线程中创建Worker对象
            const workerInstance = new worker.ThreadWorker("entry/ets/workers/Worker.ets");
            // 宿主线程向worker线程传递信息
            const buffer = new ArrayBuffer(8);
            workerInstance.postMessage(buffer, [buffer]);
            // 宿主线程接收worker线程信息
            workerInstance.onmessage = (e: MessageEvents): void => {
              // data:worker线程发送的信息
              let data: number = e.data;
              console.info("main thread data is  " + data);
              // 销毁Worker对象
              workerInstance.terminate();
            }
            // 在调用terminate后,执行onexit
            workerInstance.onexit = (code) => {
              console.log("main thread terminate");
            }

            workerInstance.onerror = (err: ErrorEvent) => {
              console.log("main error message " + err.message);
            }
          })
      }
      .width('100%')
      .height('100%')
    }
  }
}

执行结果

相关推荐
whysqwhw10 分钟前
鸿蒙Row/Column使用
harmonyos
zhanshuo12 小时前
在鸿蒙里优雅地处理网络错误:从 Demo 到实战案例
harmonyos
zhanshuo12 小时前
在鸿蒙中实现深色/浅色模式切换:从原理到可运行 Demo
harmonyos
whysqwhw17 小时前
鸿蒙分布式投屏
harmonyos
whysqwhw18 小时前
鸿蒙AVSession Kit
harmonyos
whysqwhw20 小时前
鸿蒙各种生命周期
harmonyos
whysqwhw21 小时前
鸿蒙音频编码
harmonyos
whysqwhw21 小时前
鸿蒙音频解码
harmonyos
whysqwhw21 小时前
鸿蒙视频解码
harmonyos
whysqwhw1 天前
鸿蒙视频编码
harmonyos