概述
开发者在Native侧进行开发实践时,经常会遇到一些耗时的任务,例如I/O操作、域名解析以及复杂计算等。这些任务如果直接在主线程中执行,将会严重阻塞主线程,影响后续任务的正常流程,进而导致用户界面响应延迟甚至卡顿。
为了提升代码性能,通常会将这类耗时任务放在Native子线程中执行。通常情况下,Native子线程可以独立完成自己的任务,但是很多时候需要将数据从主线程传递到Native子线程,或者将Native子线程的执行结果返回给主线程。
在多线程环境中,有一个关键问题是如何安全地在后台线程(Native侧子线程)和UI主线程之间进行通信。ArkTS函数通常只能在主线程里调用,如果Native侧通过std::thread或pthread创建了子线程,那么主线程中的上下文环境和数据(napi_env、napi_value以及napi_ref)是不能直接在子线程上下文中使用的。
为确保正确性,当Native侧在子线程完成其计算或处理后,若需要回调ArkTS函数,必须先通过线程同步机制将结果传递回主线程,然后才能安全地在主线程环境中调用ArkTS函数。针对这个问题,可以采用以下方案来解决。
说明
推荐开发者使用线程安全函数作为Native侧子线程与UI主线程的通信手段。
如果线程安全函数确实不能满足开发需要,开发者可以使用libuv库自定义Loop然后通过uv_async_send方法进行线程间通信。
另外,libuv库中的uv_queue_work接口也可以实现线程间通信,但存在以下弊端:
- 使用uv_queue_work作为线程间通信的手段时,execute回调中一般实现为空任务,没有任何维测信息,一旦异步任务不回调,定位将很困难。这种方式不仅低效,而且还增加了发生故障时定位问题的难度。
- 它会破坏底层的数据可信性,uv_queue_work函数仅用于抛异步任务,异步任务的execute回调被提交到线程池后会经过调度执行,因此并不保证多次提交的任务之间的时序关系。
实现原理
基于线程安全函数机制
HarmonyOS Node-API提供了一系列[线程安全函数]相关的接口,通过这些接口可以在Native侧创建一个可以在多线程间共享并安全使用的函数对象。在创建过程中,需要指定一些关键信息,如ArkTS回调函数、异步资源标识符、缓冲队列容量、初始线程数等。这些信息将用于确保函数在多线程环境下的正确性和安全性。通过这个机制,子线程可以将数据传递给主线程,主线程接收到数据后会调用ArkTS回调函数进行处理。
调用流程图
首先ArkTS侧会传递一个回调函数到Native侧,然后在Native侧创建一个线程安全函数,此线程安全函数会绑定一个回调函数(通过napi_call_threadsafe_function调用线程安全函数时,会触发该函数),接着需要保存后续需要用到的上下文信息及参数,然后拆分子线程(子线程绑定了要用到的上下文信息及参数)。
Native侧子线程分配到系统资源之后会执行对应的业务逻辑,通过Node-API提供的线程安全函数相关的接口调用前面声明的线程安全函数,该线程安全函数会被push到主线程的事件循环中等待事件调度执行。
线程安全函数在事件循环中得到调度后会通过napi_call_back接口调用ArkTS回调函数。
开发步骤
- ArkTS侧传递函数到Native侧
- Native侧主线程中构建线程安全函数、保存上下文信息并拆分子线程
- 在Native侧子线程中请求线程安全函数并调用
- 在线程安全函数中回调ArkTS侧传递的回调函数
基于libuv异步库的uv_async_send方法
libuv库提供了一个函数uv_async_send,用于在非阻塞事件循环中异步发送信号。uv_async_send函数允许用户在不同的线程或者事件循环的不同部分之间发送信号,从而触发某些操作而不需要直接调用阻塞函数。uv_async_send的核心原理是利用事件循环(event-loop)和内部消息队列来实现线程间的通信。具体来说,它允许一个线程(通常是工作线程)发送信号给主线程上的事件循环,从而触发主线程上的某个回调函数。因此利用该原理可以在主线程中定义用于回调Ark