线程间通信指的是并发多线程间存在的数据交换行为,目前已支持ArkTS、C++等开发语言,因此存在不同语言、不同线程的通信场景,接下来详细展开介绍。
场景描述
ArkTS线程指的是包含ArkTS运行环境的线程,包括主线程、TaskPool线程、Worker线程。它们之间可以通过不同的接口进行通信。
常见业务场景如下所示:
常见业务场景 | 具体业务描述 |
---|---|
宿主JS线程<->TaskPool线程 | 通过使用TaskPool,分发任务到子线程。TaskPool子任务与其宿主线程之间需要通信的场景 |
宿主JS线程<->Worker线程 | 通过使用Worker,启动子线程,执行任务。Worker子线程与其宿主线程之间需要通信的场景 |
任意JS线程<->任意JS线程 | 除了上述两种线程外,其他任意两个JS线程需要通信的场景 |
实现方案介绍
跨线程交互场景 | 通信方式 | 通信优先级 |
---|---|---|
宿主JS线程->TaskPool线程 | 参数传递后分发任务;过程中不支持正向通信 | 支持 |
TaskPool线程->宿主JS线程 | 结果返回;sendData触发宿主线程异步回调,底层为uv_async_send实现 | 不支持 |
宿主JS线程->Worker线程 | 采用postMessage&onmessage异步通信 | 不支持 |
Worker线程->宿主JS线程 | 异步方式:采用postMessage & onmessage异步通信同步方式:支持Worker线程同步调用宿主线程注册的方法,并返回结果 | 不支持 |
任意JS线程<->任意JS线程 | 使用@ohos.emitter实现双向异步通信 | 支持 |
业务实现中的关键点
ArkTS线程推荐使用TaskPool及Worker的原生接口通信。
与业界方案特殊差异说明
线程通信采用消息循环的机制,与业界一致。
场景描述
ArkTS线程指的是包含ArkTS运行环境的线程,包括主线程、TaskPool线程和Worker线程。由于HarmonyOS支持通过Node-API开发C++业务,用户可以在C++层创建线程,因此C++线程存在与ArkTS线程通信的场景。
常见业务场景如下所示:
常见业务场景 | 具体业务描述 |
---|---|
ArkTS线程(ArkTS)<->pthread线程 | ArkTS线程的ArkTS部分与pthread线程的通信场景 |
ArkTS线程(C++)<->pthread线程 | ArkTS线程的C++部分与pthread线程的通信场景 |
pthread线程<->pthread线程 | C++线程间的通信场景 |
实现方案介绍
跨线程交互场景 | 通信方式 | 通信优先级 |
---|---|---|
ArkTS线程(ArkTS)->pthread线程 | 不支持,需要转到C++ | 不涉及 |
pthread线程->ArkTS线程(ArkTS) | 采用napi_threadsafe_function通信 | 支持 |
pthread线程->ArkTS线程(C++) | ||
ArkTS线程(C++)-> pthread线程 | 开发者自定义行为 | 开发者自定义行为 |
pthread线程<->pthread线程 |
案例参考
// napi_init.cpp
struct CallbackData {
napi_env env;
uv_thread_t threadId;
napi_async_work asyncWork = nullptr;
napi_threadsafe_function tsfn = nullptr;
int32_t data = -1;
};
static void CallJs(napi_env env, napi_value jsCb, void* context, void* data)
{
CallbackData* callbackData = reinterpret_cast(data);
napi_value global;
assert(napi_get_global(env, &global) == napi_ok);
napi_value number;
assert(napi_create_int32(env, callbackData->data, &number) == napi_ok);
assert(napi_call_function(env, global, jsCb, 1, &number, nullptr) == napi_ok);
}
static void NativeThread(void* data)
{
CallbackData* callbackData = reinterpret_cast(data);
/* 跨线程调用*/
{
assert(napi_acquire_threadsafe_function(callbackData->tsfn) == napi_ok);
callbackData->data = 123456;
napi_status status = napi_call_threadsafe_function(callbackData->tsfn,
callbackData, napi_tsfn_blocking);
assert(status == napi_ok);
}
}
static void ThreadFinished(napi_env env, void* data, [[maybe_unused]] void* context)
{
CallbackData* callbackData = reinterpret_cast(data);
assert(uv_thread_join(&(callbackData->threadId)) == 0);
assert(napi_release_threadsafe_function(callbackData->tsfn,
napi_tsfn_release) == napi_ok);;
callbackData->asyncWork = nullptr;
callbackData->tsfn = nullptr;
delete callbackData;
}
static napi_value NativeCall(napi_env env, napi_callback_info info)
{
napi_value resourceName = nullptr;
CallbackData* callbackData = new CallbackData;
callbackData->env = env;
napi_value jsCb = nullptr;
size_t argc = 1;
assert(napi_get_cb_info(env, info, &argc, &jsCb, nullptr, nullptr) == napi_ok);
assert(argc == 1);
assert(napi_create_string_utf8(env, "Call thread-safe function from c++ thread",
NAPI_AUTO_LENGTH,
&resourceName) == napi_ok);
napi_status status;
status = napi_create_threadsafe_function(env,
jsCb,
nullptr,
resourceName,
0,
1,
callbackData,
ThreadFinished,
callbackData,
CallJs,
&(callbackData->tsfn));
assert(status == napi_ok);
assert(uv_thread_create(&(callbackData->threadId), NativeThread, callbackData) == 0);
return nullptr;
}
// Index.ets
Button('click me')
.onClick(() => {
nativeModule.nativeCall((a: number) => {
console.log('Received data from thread-function: %{public}d', a);
})
})
与业界方案特殊差异说明
不推荐应用实现方式
不建议为了同步调用,在C++层增加wait等机制,会导致卡死、掉帧等问题。
场景描述
某些进程唯一的ArkTS实例初始化流程复杂,整体耗时长,放在主线程中对其进行初始化会造成应用启动耗时久和阻塞主线程的执行。将这些实例的初始化流程放在ArkTS子线程中进行初始化,初始化完成后主线程可以直接使用该实例。
常见的业务场景如下所示:
常见业务场景 | 具体业务描述 |
---|---|
SDK初始化 | 在ArkTS子线程中调用API的Init初始化得到一个单例对象,完成后传给其他ArkTS线程使用 |
实现方案介绍(方案一)
步骤一:采用C++单例模式封装,上层封装JS壳,子线程进行初始化;
步骤二:初始化完成通知主线程,主线程导入使用该单例对象。
业务实现中的关键点
JS模块对象
模块定义好的导出对象,也就是使用者Import时获得的模块对象。
JS模块对象中的JS Function通过Node-API方法绑定至该模块的Native静态方法,调用时将调用Native静态方法来提供实际功能。
Native Instance
模块对象的成员对象(ExternalReference),由Native Class的GetCurrentInstance(标准单例实现)获得,进程内同模块都指向同一个Native单例。本设计对原有Native实现中已经提供线程安全的C++类的功能时使用,即该实例的Native成员方法也需进行同步保护。
该模块对象即使有其它JS成员,也类似于”局部变量”,即线程间并不共享。
Native静态方法
Native静态方法提供对应模块的Native功能实现,通过napi_get_cb_info获取JS Binding Function的this对象,从而通过this获取绑定在JS模块对象上的Native Instance,再调用Native Instance对应的Native成员方法,即可完成对应功能实现。
说明
同上,方法实现不可以进行全局变量的非线程安全操作。
生命周期问题
一般模块对象在主线程退出时进行析构。
若精细化控制,可绑定finalizeCallback进行管理,线程对象回收时会在该线程调用析构方法。
案例参考
// napi_init.cpp
class Singleton {
public:
static Singleton &GetInstance() {
static Singleton instance;
return instance;
}
static napi_value GetAddress(napi_env env, napi_callback_info info) {
uint64_t addressVal = reinterpret_cast(&GetInstance());
napi_value napiAddress = nullptr;
napi_create_bigint_uint64(env, addressVal, &napiAddress);
return napiAddress;
}
static napi_value GetSetSize(napi_env env, napi_callback_info info) {
std::lock_guard lock(Singleton::GetInstance().numberSetMutex_);
uint32_t setSize = Singleton::GetInstance().numberSet_.size();
napi_value napiSize = nullptr;
napi_create_uint32(env, setSize, &napiSize);
return napiSize;
}
static napi_value Store(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value args[1] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
if (argc != 1) {
napi_throw_error(env, "ERROR: ", "store args number must be one");
return nullptr;
}
napi_valuetype type = napi_undefined;
napi_typeof(env, args[0], &type);
if (type != napi_number) {
napi_throw_error(env, "ERROR: ", "store args is not number");
return nullptr;
}
std::lock_guard lock(Singleton::GetInstance().numberSetMutex_);
uint32_t value = 0;
napi_get_value_uint32(env, args[0], &value);
Singleton::GetInstance().numberSet_.insert(value);
return nullptr;
}
private:
Singleton() {} // 私有构造函数,防止外部实例化对象
Singleton(const Singleton &) = delete; // 禁止拷贝构造函数
Singleton &operator=(const Singleton &) = delete; // 禁止赋值运算符
public:
std::unordered_set numberSet_{};
std::mutex numberSetMutex_{};
};
// Index.ets
import singleton from 'libentry.so';
import { taskpool } from '@kit.ArkTS';
@Concurrent
function getAddress() {
let address = singleton.getAddress();
console.info("taskpool:: address is " + address);
}
@Concurrent
function store(a: number, b: number, c: number) {
let size = singleton.getSetSize();
console.info("set size is " + size + " before store");
singleton.store(a);
singleton.store(b);
singleton.store(c);
size = singleton.getSetSize();
console.info("set size is " + size + " after store");
}
@Entry
@Component
struct Index {
build() {
Row() {
Column() {
Button("TestSingleton").onClick(() => {
let address = singleton.getAddress();
console.info("host thread address is " + address);
let task1 = new taskpool.Task(getAddress);
taskpool.execute(task1);
let task2 = new taskpool.Task(store, 1, 2, 3);
taskpool.execute(task2);
let task3 = new taskpool.Task(store, 4, 5, 6);
taskpool.execute(task3);
})
}
.width('100%')
}
.height('100%')
}
}
实现方案介绍(方案二)
步骤一:采用ArkTS原生对象,定义Sendable类的单例,封装成共享模块(进程内共享),子线程进行初始化;
步骤二:初始化完成通知主线程,主线程导入使用该单例对象。
业务实现中的关键点
Sendable类需要满足一定的约束
案例参考
// Demo.ets
@Sendable
export class Demo {
private static instance: Demo;
private constructor() {
}
public static getInstance(): Demo {
if (!Demo.instance) {
Demo.instance = new Demo();
}
return Demo.instance;
}
public init(): void {
// 初始化逻辑
}
}
// xxx.ets
import { Demo } from './demo';
import { taskpool } from '@kit.ArkTS';
@Concurrent
function initSingleton(): void {
let demo = Demo.getInstance();
demo.init();
// 通知主线程初始化完成
}
async function executeTaskPool(): Promise {
let task = new taskpool.Task(initSingleton);
await taskpool.execute(task);
}
executeTaskPool();
与业界方案特殊差异说明
Java存在ClassLoader机制,所有类型是静态且唯一的,因此可以很方便的导入类,支持单例模式。而HarmonyOS APP开发时需要借助共享模块,保证类只加载一次,保证唯一性。
场景描述
定义为Sendable类型的对象在发送到其他TS线程后可被多线程读写,开发者需要通过异步锁机制进行管理。需要一种能力保障对象的数据被多线程访问时准确,要么通过锁机制要么使对象变成只读对象。
常见的业务场景如下所示:
常见业务场景 | 具体业务描述 |
---|---|
全局环境变量共享 | 应用启动时生成一些资源加载入口、配置参数、全局变量等不需要更新的变量,可通过冻结能力冻结后共享到多个ArkTS子线程 |
一次性产物不可变共享 | 业务阶段性产生的页面布局数据,这个数据是在工作线程生成的,传输并缓存在UI线程后不会修改,可能会多次作为UI渲染的输入使用 |
实现方案介绍
通过冻结API,使共享对象变成只读对象。实现方案介绍:
步骤一:业务逻辑定义、生成需要的Sendable对象;
步骤二:发送到其他ArkTS线程前通过Object.Freeze API冻结该对象;
步骤三:通过taskpool或worker的消息通信机制将该对象共享到其他ArkTS线程。
业务实现中的关键
冻结后对象不可修改,如果修改会抛出ArkTS异常。
案例参考
以全局环境变量共享为例:
// xxx.ets
import { worker } from '@kit.ArkTS';
import { freezeObj } from './freezeObj';
@Sendable
export class GlobalConfig {
// 一些配置属性与方法
init() {
// 初始化相关逻辑
freezeObj(this) // 初始化完成后冻结当前对象
}
}
let globalConfig = new GlobalConfig();
globalConfig.init();
const workerInstance = new worker.ThreadWorker('entry/ets/workers/Worker.ets`', { name: 'Worker1' });
workerInstance.postMessage(globalConfig);
// worker文件路径为:entry/ets/workers/Worker.ets
// Worker.ets
import { MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';
import { GlobalConfig } from '../pages/InterthreadCommunication4';
const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
workerPort.onmessage = (e: MessageEvents) => {
let globalConfig: GlobalConfig = e.data;
// 使用globalConfig对象
}
// freezeObj.ts
export function freezeObj(obj: any) {
Object.freeze(obj);
}
与业界方案特殊差异说明
内存共享模型如Java/C++对象在不同线程间都是可见的,sendable对象(共享)需要将对象引用发送到其他线程才可使用。
场景描述
生产者与消费者模式表现为以下几个特征:
有复数或单数个生产者并发地生产数据;
有复数或单数个消费者并发地消费数据;
存在一个数据缓存区,生产者生产出的数据存储在缓存区,消费者从缓存区中取数据,当缓存区满的时候要通知生产者停止生产,当缓存区为空时通知消费者休眠直到生产者添加数据。
常见的业务场景如下所示:
常见业务场景 | 具体业务描述 | 场景类型 |
---|---|---|
阅读应用页面预加载 | 用户每次翻页或跳转后需要预加载复数张前后页,将前后页的加载请求缓存到一个加载队列中,将队列中的页面布局解析任务并发地执行 | CPU密集型 + IO密集型 |
本地文件上传 | 用户在主线程一次上传单个或复数个文件,上传文件的请求被储存在一个上传队列中,并发地将队列中的文件上传到云端 | CPU密集型+ IO密集型 |
实现方案介绍
以阅读应用场景为例:
步骤一:用户一次翻页产生复数个前后页预加载的请求;
步骤二:通过网络接口从云端下载复数页面的原始数据;
步骤三:通过taskpool并发地解析每一页的页面原始数据生成page对象,page对象描述了页面的布局信息和每个组成部分;
步骤四:taskpool执行的结果返回到UI线程的缓存队列中;
步骤五:缓存队列中的页面数据中临近用户当前页的page对象执行渲染任务。
业务实现中的关键
与业界方案特殊差异说明
内存共享模型如Java/C++对象在不同线程间都是可见的,ArkTS是线程间内存隔离的内存模型对象在不同线程间使用需要序列化(拷贝),sendable对象(共享)需要将对象引用发送到其他线程才可使用。
sendable对象存在较多约束,尽量只将必须共享的对象定义为sendable对象,由普通的ArkTS对象持有sendable对象并将整个流程串起来。