#2023盲盒+码# 线程间通讯知识学习 原创

zhushangyuan_
发布于 2023-9-17 11:04
浏览
0收藏

【本文正在参加 2023「盲盒」+码有奖征文活动】,活动链接 https://ost.51cto.com/posts/25284

线程间通信

通信方式

使用Worker多线程方案有两种线程间通信方式:

  1. 直接使用Worker进行线程间通信;
  2. 使用Emitter进行线程间通信;

1. 直接使用Worker进行线程间通信

第一种方式是直接使用Worker提供的接口进行主线程和Worker线程的通信,这种通信方式只有在使用Worker的情况下才能使用,TaskPool我们推荐使用第二中通信方式。下面介绍Stage模型下的同时方法;

主线程中处理和发送事件的示例代码如下:

import worker from '@ohos.worker';

let wk = new worker.ThreadWorker("entry/ets/workers/worker.ts");

// 发送消息到worker线程
wk.postMessage("message from main thread.")

// 处理来自worker线程的消息
wk.onmessage = (message) => {
  console.info("message from worker: " + message)

  // 根据业务按需停止worker线程
  wk.terminate();
}

Worker中处理和发送事件的示例代码如下:

import worker from '@ohos.worker';

let parent = worker.workerPort;

// 处理来自主线程的消息
parent.onmessage = (message) => {
  console.info("onmessage: " + message)
  // 发送消息到主线程
  parent.postMessage("message from worker thread.")
}

2.使用Emitter进行线程间通信

Emitter主要提供线程间发送和处理事件的能力,包括对持续订阅事件或单次订阅事件的处理、取消订阅事件、发送事件到事件队列等。

Emitter的开发步骤如下:

订阅事件

import emitter from "@ohos.events.emitter";

// 定义一个eventId为1的事件
let event: emitter.InnerEvent = {
  eventId: 1
};

// 收到eventId为1的事件后执行该回调
let callback = (eventData: emitter.EventData) => {
  console.info('event callback');
};

// 订阅eventId为1的事件
emitter.on(event, callback);

发送事件

import emitter from "@ohos.events.emitter";

// 定义一个eventId为1的事件,事件优先级为Low
let event: emitter.InnerEvent = {
  eventId: 1,
  priority: emitter.EventPriority.LOW
};

let data = new Map<string, Object>();
data.set("content", "c");
data.set("id", 1);
data.set("isEmpty", false);
let eventData: emitter.EventData = {data};

// 发送eventId为1的事件,事件内容为eventData
emitter.emit(event, eventData);

支持的数据传输对象类型

目前支持传输的数据对象可以分为普通对象、可转移对象、可共享对象、Native绑定对象四种。

普通对象:普通对象传输采用标准的结构化克隆算法(Structured Clone)进行序列化,此算法可以通过递归的方式拷贝传输对象,相较于其他序列化的算法,支持的对象类型更加丰富。序列化支持的类型包括:除Symbol之外的基础类型、Date、String、RegExp、Array、Map、Set、Object(仅限简单对象,比如通过“{}”或者“new Object”创建,普通对象仅支持传递属性,不支持传递其原型及方法)、ArrayBuffer、TypedArray。

可转移对象:可转移对象(Transferable object)传输采用地址转移进行序列化,不需要内容拷贝,会将ArrayBuffer的所有权转移给接收该ArrayBuffer的线程,转移后该ArrayBuffer在发送它的线程中变为不可用,不允许再访问。

可共享对象:共享对象SharedArrayBuffer,拥有固定长度,可以存储任何类型的数据,包括数字、字符串等。共享对象传输指SharedArrayBuffer支持在多线程之间传递,传递之后的SharedArrayBuffer对象和原始的SharedArrayBuffer对象可以指向同一块内存,进而达到内存共享的目的。
SharedArrayBuffer对象存储的数据在同时被修改时,需要通过原子操作保证其同步性,即下个操作开始之前务必需要等到上个操作已经结束。

Native绑定对象:Native绑定对象(Native Binding Object)是系统所提供的对象,该对象与底层系统功能进行绑定,提供直接访问底层系统功能的能力。例如Context和RemoteObject等。

其他场景的示例和方案思考

在日常开发过程中,我们还会碰到一些其他并发场景问题,下面我们介绍了一些常用并发场景的示例方案推荐。

子线程调用主线程类型的方法

我们在主线程中创建并了一个对象,假如类型为MyMath,我们需要把这个对象传递到子线程中,然后在子线程中执行该类型中的一些耗时操作方法,比如Math中的compute方法,示意代码如下:

类结构:

class MyMath {
  a: number = 0;
  b: number = 1;

  constructor(a: number, b: number) {
    this.a = a;
    this.b = b;
  }

  compute() {
    return this.a + this.b;
  }
}

主线程代码:

private math: MyMath = new MyMath(2, 3); // 初始化a和b的值为2和3
private workerInstance: worker.ThreadWorker;

this.workerInstance = new worker.ThreadWorker("entry/ets/worker/MyWorker.ts");
this.workerInstance.postMessage(this.math); // 发送到子线程中,期望执行MyMath中的compute方法,预期值是2+3=5

MyMath对象在进行线程传递后,会丢失类中的方法属性,导致我们只是在子线程中可以获取到MyMath的数据,但是无法在子系统中直接调用MyMath的compute方法,示意代码如下:

const workerPort = worker.workerPort;
workerPort.onmessage = (e: MessageEvents): void => {
  let a = e.data.a;
  let b = e.data.b;
}

这种情况下我们可以怎么去实现在子线程中调用主线程中类的方法呢?

首先,我们尝试使用强制转换的方式把子线程接收到数据强制转换成MyMath类型,示例代码如下:

const workerPort = worker.workerPort;
workerPort.onmessage = (e: MessageEvents): void => {
  let math = e.data as MyMath; // 方法一:强制转换
  console.log('math compute:' + math.compute()); // 执行失败,不会打印此日志
}

但是结果是失败的,于是我们想到了第二种方法,把数据进行重新构造初始化一个新的MyMath对象,然后执行compute方法,示例代码如下:

const workerPort = worker.workerPort;
workerPort.onmessage = (e: MessageEvents): void => {
  // 重新构造原类型的对象
  let math = new MyMath(0, 0);
  math.a = e.data.a;
  math.b = e.data.b;
  console.log('math compute:' + proxy.compute()); // 成功打印出结果:5
}

第二种方法成功在子线程中调用了MyMath的compute方法。但是这种方式还有弊端,比如每次使用到这个类进行传递,我们就得重新进行构造初始化,而且构造的代码会分散到工程的各处,很难进行维护,于是我们有了第三种改进方案。

第三种方法,我们需要构造一个接口类,包含了我们需要进程间调用的基础方法,示例代码如下:

interface MyMathInterface {
  compute():number;
}

然后,我们把MyMath类继承这个方法,并且额外构造一个代理类,继承MyMath类,示例代码如下:

class MyMath implements MyMathInterface {
  a: number = 0;
  b: number = 1;

  constructor(a: number, b: number) {
    console.log('MyMath constructor a:' + a + ' b:' + b)
    this.a = a;
    this.b = b;
  }

  compute() {
    return this.a + this.b;
  }
}

class MyMathProxy extends MyMath {
  constructor(math: any) {
    super(math.a, math.b);
  }
}

我们在主线程构造并且传递MyMath对象后,在子线程中转换成MyMathProxy,即可调用到MyMath的compute方法了,并且无需在多处进行初始化构造,只要把构造逻辑放到MyMathProxy或者MyMath的构造函数中,子线程中的示例代码如下:

const workerPort = worker.workerPort;
workerPort.onmessage = (e: MessageEvents): void => {
  // 方法三:使用代理类构造对象
  let proxy = new MyMathProxy(e.data)
  console.log('math compute:' + proxy.compute()); // 成功打印出结果:5
}

Worker和TaskPool对比

实现 TaskPool Worker
内存模型 线程间隔离,内存不共享。 线程间隔离,内存不共享。
参数传递机制 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。
参数传递 直接传递,无需封装,默认进行transfer。 消息对象唯一参数,需要自己封装。
方法调用 直接将方法传入调用。 在Worker线程中进行消息解析并调用对应方法。
返回值 异步调用后默认返回。 主动发送消息,需在onmessage解析赋值。
生命周期 TaskPool自行管理生命周期,无需关心任务负载高低。 开发者自行管理Worker的数量及生命周期。
任务池个数上限 自动管理,无需配置。 最多开启8个Worker。
任务执行时长上限 3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时)。 无限制。
设置任务的优先级 支持配置任务优先级。 不支持。
执行任务的取消 支持取消已经发起的任务。 不支持。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
标签
收藏
回复
举报
回复
    相关推荐