簡介
在OpenHarmony應用開發(fā)實踐中,經常會遇到一些耗時的任務充包,如I/O操作副签、域名解析以及復雜計算等。這些任務如果直接在主線程中執(zhí)行基矮,將會嚴重阻塞主線程继薛,影響后續(xù)任務的正常流程,進而導致用戶界面響應延遲甚至卡頓愈捅。因此遏考,為了提升代碼性能,通常會將這類耗時任務放在子線程中執(zhí)行蓝谨。
本文將聚焦于如何利用native的方式實現(xiàn)跨線程調用灌具,即采用線程安全函數(shù)和libuv異步I/O工具庫這兩種策略,來優(yōu)化程序性能并保持流暢的用戶體驗譬巫。
注意事項
以下將詳細闡述如何運用native方式創(chuàng)建子線程以執(zhí)行耗時任務咖楣,并確保與JavaScript的無縫交互。為此芦昔,開發(fā)者可以利用arkui_napi倉庫提供的NAPI(Node-API)接口來實現(xiàn)跨語言調用的橋梁诱贿。該NAPI的設計嚴格遵循Node.js的NAPI規(guī)范,以便開發(fā)者能夠更輕松地理解和使用咕缎。
特別強調的是珠十,JavaScript函數(shù)通常只能在主線程里調用。如果native側通過std::thread或pthread創(chuàng)建了子線程凭豪,那么napi_env地梨、napi_value以及napi_ref是不能直接在子線程上下文中使用的携悯。為確保正確性计贰,當native端在子線程完成其計算或處理后聂抢,若需要回調JavaScript函數(shù)拯钻,必須先通過線程同步機制將結果傳遞回主線程,然后才能安全地在主線程環(huán)境中調用JavaScript函數(shù)撰豺。
為解決這一問題粪般,以下將提出兩種有效的解決方案。
解決方案
線性安全函數(shù)
napi_threadsafe_function 提供了接口來創(chuàng)建一個可以在多線程間共享并安全使用的函數(shù)對象污桦。通過這個機制亩歹,子線程可以將數(shù)據(jù)傳遞給主線程,主線程接收到數(shù)據(jù)后會調用JavaScript回調函數(shù)進行處理寡润。該接口包含用于創(chuàng)建捆憎、銷毀線程安全函數(shù)以及在其之間發(fā)送消息和同步數(shù)據(jù)的方法。使用napi_threadsafe_function的一般步驟包括:
創(chuàng)建線程安全函數(shù): 通過調用napi_create_threadsafe_function()創(chuàng)建一個線程安全函數(shù)對象梭纹。在此過程中躲惰,需要指定一個JavaScript回調函數(shù),該函數(shù)將在主線程上執(zhí)行变抽;同時設定相關的上下文信息础拨,這個上下文可以在多個線程之間共享,可以隨時通過調用napi_get_threadsafe_function_context()來獲取绍载。此外诡宗,還可以選擇性地提供一個napi_finalize回調,用于在銷毀線程安全函數(shù)時執(zhí)行資源清理操作击儡。
獲取使用權: 在開始使用線程安全函數(shù)之前塔沃,調用napi_acquire_threadsafe_function()函數(shù)表明線程已準備就緒,可以開始對該線程安全函數(shù)進行操作阳谍。
從子線程調用回調: 在子線程中蛀柴,通過調用napi_call_threadsafe_function()來異步觸發(fā)JavaScript回調函數(shù),并將所需數(shù)據(jù)作為參數(shù)傳遞給該回調函數(shù)矫夯。調用會被排隊鸽疾,并最終在JavaScript主線程上執(zhí)行。
資源清理: 當線程安全函數(shù)不再需要時训貌,應當正確地釋放和清理與其關聯(lián)的資源制肮。通常調用napi_release_threadsafe_function()函數(shù)來完成的,該函數(shù)會按照預定的策略處理尚未執(zhí)行完畢的回調递沪,并最終銷毀線程安全函數(shù)對象豺鼻。
延長生命周期
在JavaScript層面?zhèn)鬟f給native層的函數(shù)引用,其生命周期僅限于它所在的作用域內区拳。若要確保在超出該作用域后仍能繼續(xù)使用這個函數(shù)引用拘领,需要采取適當?shù)姆椒▉硌娱L其生命周期。
可以通過調用napi_create_reference為JavaScript對象創(chuàng)建一個引用(reference)樱调。這樣可以避免對象因垃圾回收機制而被提前釋放约素,從而有效地延長它的生命周期。然而笆凌,在創(chuàng)建引用之后圣猎,務必牢記要在不再需要該引用時,調用napi_delete_reference來釋放引用乞而,以防止內存泄漏問題的發(fā)生送悔。
深入理解并妥善管理JavaScript與native接口之間對象的生命周期,對于編寫高效且無內存泄漏隱患的代碼至關重要爪模。建議開發(fā)者進一步研究生命周期管理相關文檔和最佳實踐欠啤,以便更好地掌握。
libuv
libuv是一個基于事件驅動的異步I/O庫屋灌,對于耗時操作洁段,如果直接在libuv的主循環(huán)(event loop)中處理,會阻塞后續(xù)任務的執(zhí)行共郭。為解決這個問題祠丝,libuv內部維護了一個線程池,用于執(zhí)行一些耗時操作除嘹,并在這些操作完成后写半,將回調函數(shù)添加回主線程的event loop中等待執(zhí)行。
默認情況下尉咕,libuv提供的線程池包含4個線程作為基本工作單元叠蝇,但最大線程數(shù)可以擴展到128個。通過預先設置環(huán)境變量 UV_THREADPOOL_SIZE 的值年缎,可以自定義線程池中的線程數(shù)量悔捶。當線程池初始化時,會創(chuàng)建相應數(shù)量的工作線程晦款,并在每個線程內部運行一個 uv_queue_work 函數(shù)炎功。
值得注意的是,libuv 中的線程池是全局共享資源缓溅,不論應用中有多少個獨立的事件循環(huán)實例蛇损,它們都共用同一個線程池。這樣的設計旨在有效利用系統(tǒng)資源坛怪,同時避免因頻繁創(chuàng)建和銷毀線程帶來的開銷淤齐。
uv_queue_work
uv_queue_work(uv_loop_t* loop,
uv_work_t* req,
uv_work_cb work_cb,
uv_after_work_cb after_work_cb);
初始化一個工作請求,通過調用uv_queue_work函數(shù)袜匿,可以安排指定的任務更啄,在與事件循環(huán)(event loop)關聯(lián)的線程池中的一個線程上執(zhí)行。一旦該任務(即work_cb回調函數(shù))完成其操作居灯,將在事件循環(huán)線程中調用另一個回調函數(shù)after_work_cb祭务。
各參數(shù)的具體意義如下:
loop: 指向事件循環(huán)結構體的指針内狗,所有異步操作都在這個事件循環(huán)上下文中進行管理。
req: 指向uv_work_t結構體的指針义锥,用于傳遞給工作請求和回調函數(shù)的數(shù)據(jù)柳沙。通常開發(fā)者會將自定義數(shù)據(jù)賦值給req->data成員變量以在回調中使用。
work_cb: 執(zhí)行實際工作的回調函數(shù)拌倍,一些耗時的操作可以在此執(zhí)行赂鲤,該函數(shù)在線程池的一個線程上運行。
after_work_cb: 工作完成后在事件循環(huán)線程上調用的回調函數(shù)柱恤,常用于處理work_cb執(zhí)行結果或觸發(fā)進一步的JavaScript層面的操作数初。
需要注意的是,盡管uv_queue_work方法本身不直接涉及NAPI(Node-API)接口梗顺,但當涉及到與JavaScript線程交互時泡孩,特別是從native層向JavaScript層傳遞數(shù)據(jù)并觸發(fā)回調時,需要正確地管理napi_value對象的生命周期荚守。這需要合理使用napi_handle_scope和相關接口珍德,來確保在JavaScript回調方法創(chuàng)建的napi_value對象,在整個執(zhí)行過程中保持有效矗漾,并在適當?shù)臅r候釋放資源锈候,以避免內存泄漏問題。
示例代碼
下面的示例分別用線程安全函數(shù)和libuv實現(xiàn)了native的跨線程調用敞贡。該示例在ArkTS端傳入的JavaScript回調函數(shù)中對變量value進行加10運算泵琳,在native側開啟了3個子線程執(zhí)行業(yè)務邏輯,子線程業(yè)務邏輯完成之后回到主線程執(zhí)行ArkTS端傳入的JavaScript回調函數(shù)誊役,從而完成了對ArkTS端變量value的加30操作获列。
1.使用線程安全函數(shù)
ArkTS實現(xiàn)一個JavaScript回調函數(shù)。
參數(shù)為param蛔垢,函數(shù)體中對參數(shù)param加10后綁定變量value击孩,并返回最新的param值。將回調函數(shù)作為參數(shù)調用native側的ThreadSafeTest接口鹏漆。
// src/main/ets/pages/Index.ets
Button("threadSafeTest")
.width('40%')
.fontSize(20)
.onClick(()=> {
// native使用線程安全函數(shù)實現(xiàn)跨線程調用
entry.ThreadSafeTest((param: number) => {
param += 10;
logger.info('ThreadSafeTest js callback value = ', param.toString());
this.value = param;
return param;
}
)
}).margin(20)
native主線程中實現(xiàn)一個ThreadSafeTest接口巩梢。
接口接收到ArkTS傳入的JavaScript回調函數(shù)后通過napi_create_threadsafe_function創(chuàng)建一個線程安全函數(shù)tsfn,tsfn會回調主線程中的ThreadSafeCallJs艺玲,然后在ThreadSafeCallJs中調用ArkTS端傳入的JavaScript回調函數(shù)括蝠。
// src/main/cpp/hello.cpp
napi_threadsafe_function tsfn; // 線程安全函數(shù)
static int g_cValue; // 保存value最新的值,作為參數(shù)傳給js回調函數(shù)
int g_threadNum = 3; // 線程數(shù)
struct CallbackContext {
napi_env env = nullptr;
napi_ref callbackRef = nullptr;
int retData = 0;
};
// 安全函數(shù)回調
static void ThreadSafeCallJs(napi_env env, napi_value js_cb, void *context, void *data)
{
CallbackContext *argContent = (CallbackContext *)data;
if (argContent != nullptr) {
OH_LOG_INFO(LOG_APP, "ThreadSafeTest CallJs start, retData:[%{public}d]", argContent->retData);
napi_get_reference_value(env, argContent->callbackRef, &js_cb);
} else {
OH_LOG_INFO(LOG_APP, "ThreadSafeTest CallJs argContent is null");
return;
}
napi_valuetype valueType = napi_undefined;
napi_typeof(env, js_cb, &valueType);
if (valueType != napi_valuetype::napi_function) {
OH_LOG_ERROR(LOG_APP, "ThreadSafeTest callback param is not function");
if (argContent != nullptr) {
napi_delete_reference(env, argContent->callbackRef);
delete argContent;
argContent = nullptr;
OH_LOG_INFO(LOG_APP, "ThreadSafeTest delete argContent");
}
return;
}
// 將當前value值作為參數(shù)調用js函數(shù)
napi_value argv;
napi_create_int32(env, g_cValue, &argv);
napi_value result = nullptr;
napi_call_function(env, nullptr, js_cb, 1, &argv, &result);
// g_cValue保存調用js后的返回結果
napi_get_value_int32(env, result, &g_cValue);
OH_LOG_INFO(LOG_APP, "ThreadSafeTest CallJs end, [%{public}d]", g_cValue);
if (argContent != nullptr) {
napi_delete_reference(env, argContent->callbackRef);
delete argContent;
argContent = nullptr;
OH_LOG_INFO(LOG_APP, "ThreadSafeTest delete argContent end");
}
}
// 使用安全函數(shù)跨線程調用js函數(shù)
static napi_value ThreadSafeTest(napi_env env, napi_callback_info info)
{
size_t argc = 1;
napi_value js_cb;
napi_value workName;
// 獲取ArkTS 參數(shù)
napi_get_cb_info(env, info, &argc, &js_cb, nullptr, nullptr);
// 判斷參數(shù)類型
napi_valuetype valueType = napi_undefined;
napi_typeof(env, js_cb, &valueType);
if (valueType != napi_valuetype::napi_function) {
OH_LOG_ERROR(LOG_APP, "ThreadSafeTest callback param is not function");
return nullptr;
}
OH_LOG_INFO(LOG_APP, "ThreadSafeTest current value: [%{public}d]", g_cValue);
// 使用安全線程跨線程調用js 函數(shù)
napi_create_string_utf8(env, "workItem", NAPI_AUTO_LENGTH, &workName);
// 創(chuàng)建線程安全函數(shù)
napi_create_threadsafe_function(env, js_cb, NULL, workName, 0, 1, NULL, NULL, NULL, ThreadSafeCallJs, &tsfn);
在native子線程中調用線程安全函數(shù)。
通過std::thread創(chuàng)建子線程饭聚,在子線程中通過napi_call_threadsafe_function調用線程安全函數(shù)tsfn忌警,把CallbackContext 結構體數(shù)據(jù)作為參數(shù)傳入ThreadSafeCallJs。這里在子線程中進行了簡單的業(yè)務處理秒梳,開發(fā)者可以根據(jù)自身實際需求進行相應的業(yè)務操作法绵。
// src/main/cpp/hello.cpp
// 在子線程中調用線程安全函數(shù)
for (int i = 0; i < g_threadNum; i++) {
// 創(chuàng)建回調參數(shù)
auto asyncContext = new CallbackContext();
asyncContext->env = env;
asyncContext->retData = i;
napi_create_reference(env, js_cb, 1, &asyncContext->callbackRef);
std::thread t([asyncContext]() {
// 處理業(yè)務邏輯
OH_LOG_INFO(LOG_APP, "ThreadSafeTest ChildTread start, index:[%{public}d], value: [%{public}d]",
asyncContext->retData, g_cValue);
asyncContext->retData++;
// 請求線程安全函數(shù)
napi_acquire_threadsafe_function(tsfn);
// 調用線程安全函數(shù)
napi_call_threadsafe_function(tsfn, asyncContext, napi_tsfn_nonblocking);
OH_LOG_INFO(LOG_APP, "ThreadSafeTest ChildTread end, index:[%{public}d], value: [%{public}d]",
asyncContext->retData, g_cValue);
/* 以下直接在子線程中調用js函數(shù),會崩潰
napi_value result = nullptr;
napi_value argv;
napi_create_int32(env,g_cValue, &argv);
napi_call_function(env, nullptr, js_cb, 1, &argv, &result);
*/
});
t.join();
}
// 釋放安全線程
napi_status status = napi_release_threadsafe_function(tsfn, napi_tsfn_release);
if (status == napi_status::napi_ok) {
OH_LOG_INFO(LOG_APP, "ThreadSafeTest napi_tsfn_release success.");
} else {
OH_LOG_INFO(LOG_APP, "ThreadSafeTest napi_tsfn_release fail !");
}
2.使用libuv
ArkTS實現(xiàn)一個JavaScript回調函數(shù)箕速。
參數(shù)為param,函數(shù)體中對參數(shù)param加10后綁定變量value礼烈,并返回最新的param值弧满。然后將回調函數(shù)作為參數(shù)調用native側的UvWorkTest接口婆跑。
// src/main/ets/pages/Index.ets
Button("libuvTest")
.width('40%')
.fontSize(20)
.onClick(()=> {
// native使用線程安全函數(shù)實現(xiàn)跨線程調用
entry.UvWorkTest((param: number) => {
param += 10;
logger.info('UvWorkTest js callback value = ', param.toString());
this.value = param;
return param;
}
)
}).margin(20)
native主線程中實現(xiàn)一個UvWorkTest接口此熬。
接口接收到ArkTS傳入的JavaScript回調函數(shù)后創(chuàng)建子線程,在子線程的執(zhí)行函數(shù)CallbackUvWorkTest中創(chuàng)建工作任務workReq滑进,通過uv_queue_work將工作任務添加到libuv隊列中犀忱。
// src/main/cpp/hello.cpp
void CallbackUvWorkTest(CallbackContext *context)
{
if (context == nullptr) {
OH_LOG_ERROR(LOG_APP, "UvWorkTest context is nullptr");
return;
}
uv_loop_s *loop = nullptr;
napi_get_uv_event_loop(context->env, &loop);
// 創(chuàng)建工作數(shù)據(jù)結構,自定義數(shù)據(jù)結構添加在data中
uv_work_t *workReq = new uv_work_t;
if (workReq == nullptr) {
if (context != nullptr) {
napi_delete_reference(context->env, context->callbackRef);
delete context;
OH_LOG_INFO(LOG_APP, "UvWorkTest delete context");
context = nullptr;
}
OH_LOG_ERROR(LOG_APP, "UvWorkTest new uv_work_t fail!");
return;
}
workReq->data = (void *)context;
// 此打印位于子線程
OH_LOG_INFO(LOG_APP, "UvWorkTest childThread_1 [%{public}d]", g_cValue);
// 添加工作任務到libuv的隊列中
uv_queue_work(loop, workReq, WorkCallback, AfterWorkCallback);
}
// 使用uv_work callback 實現(xiàn)跨線程調用js函數(shù)
static napi_value UvWorkTest(napi_env env, napi_callback_info info)
{
size_t argc = 1;
napi_value argv[1] = {0};
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
napi_valuetype valueType = napi_undefined;
napi_typeof(env, argv[0], &valueType);
if (valueType != napi_function) {
OH_LOG_ERROR(LOG_APP, "UvWorkTest param is not function");
return nullptr;
}
OH_LOG_INFO(LOG_APP, "UvWorkTest current value:[%{public}d]", g_cValue);
for (int i = 0; i < g_threadNum; i++) {
auto asyncContext = new CallbackContext();
if (asyncContext == nullptr) {
OH_LOG_ERROR(LOG_APP, "UvWorkTest new asyncContext fail!");
return nullptr;
}
asyncContext->env = env;
asyncContext->retData = i;
OH_LOG_INFO(LOG_APP, "UvWorkTest thread begin index:[%{public}d], value:[%{public}d]", i, g_cValue);
napi_create_reference(env, argv[0], 1, &asyncContext->callbackRef);
// using callback function on other thread
std::thread testThread(CallbackUvWorkTest, asyncContext);
testThread.detach();
OH_LOG_INFO(LOG_APP, "UvWorkTest thread end index:[%{public}d], value:[%{public}d]", i, g_cValue);
}
return nullptr;
}
實現(xiàn)work_cb與after_work_cb。
work_cb位于子線程中扶关,執(zhí)行實際的業(yè)務邏輯阴汇;after_work_cb位于主線程中,通過napi_call_function調用ArkTS端傳入的JavaScript回調函數(shù)节槐。
// src/main/cpp/hello.cpp
void WorkCallback(uv_work_t *workReq)
{
// 另外一個子線程,一些耗時操作可以在此進行. 此處不能調用js函數(shù).
CallbackContext *context = (CallbackContext *)workReq->data;
if (context != nullptr) {
OH_LOG_INFO(LOG_APP, "UvWorkTest CallBack1 childThread_2 [%{public}d]", context->retData);
context->retData++;
OH_LOG_INFO(LOG_APP, "UvWorkTest CallBack2 childThread_2 [%{public}d]", context->retData);
} else {
OH_LOG_INFO(LOG_APP, "UvWorkTest CallBack3 childThread_2 context is null.");
}
}
void AfterWorkCallback(uv_work_t *workReq, int status)
{
CallbackContext *context = (CallbackContext *)workReq->data;
// 主線程執(zhí)行搀庶,可以在此調用js函數(shù)
OH_LOG_INFO(LOG_APP, "UvWorkTest CallBack mainThread [%{public}d]", context->retData);
napi_handle_scope scope = nullptr;
napi_open_handle_scope(context->env, &scope);
if (scope == nullptr) {
if (context != nullptr) {
napi_delete_reference(context->env, context->callbackRef);
delete context;
context = nullptr;
}
if (workReq != nullptr) {
delete workReq;
workReq = nullptr;
}
return;
}
napi_value callback = nullptr;
napi_get_reference_value(context->env, context->callbackRef, &callback);
napi_value retArg;
OH_LOG_INFO(LOG_APP, "UvWorkTest CallBack begin [%{public}d]", g_cValue);
napi_create_int32(context->env, g_cValue, &retArg);
napi_value ret;
napi_call_function(context->env, nullptr, callback, 1, &retArg, &ret);
// 保存js回調結果
napi_get_value_int32(context->env, ret, &g_cValue);
OH_LOG_INFO(LOG_APP, "UvWorkTest CallBack end [%{public}d]", g_cValue);
napi_close_handle_scope(context->env, scope);
if (context != nullptr) {
napi_delete_reference(context->env, context->callbackRef);
delete context;
OH_LOG_INFO(LOG_APP, "UvWorkTest delete context");
context = nullptr;
}
if (workReq != nullptr) {
delete workReq;
OH_LOG_INFO(LOG_APP, "UvWorkTest delete work");
workReq = nullptr;
}
}
總結
線程安全函數(shù)和libuv方案都是在子線程的執(zhí)行函數(shù)運行結束后回到主線程,并將JavaScript回調函數(shù)push到主線程的event-loop隊列里等待被執(zhí)行铜异。
兩者的差異在于libuv的子線程屬于libuv線程池哥倔,而線程安全函數(shù)的子線程需要根據(jù)業(yè)務要求自己創(chuàng)建。另外在libuv中揍庄,JavaScript回調函數(shù)只能在子線程的主函數(shù)執(zhí)行完畢后被動被執(zhí)行咆蒿;而在線程安全函數(shù)中,JavaScript回調函數(shù)則可以在任意線程中主動調用蚂子。