簡介
在OpenHarmony應(yīng)用開發(fā)實踐中,經(jīng)常會遇到一些耗時的任務(wù)姊舵,如I/O操作晰绎、域名解析以及復(fù)雜計算等。這些任務(wù)如果直接在主線程中執(zhí)行括丁,將會嚴重阻塞主線程荞下,影響后續(xù)任務(wù)的正常流程,進而導(dǎo)致用戶界面響應(yīng)延遲甚至卡頓躏将。因此锄弱,為了提升代碼性能,通常會將這類耗時任務(wù)放在子線程中執(zhí)行祸憋。
本文將聚焦于如何利用native的方式實現(xiàn)跨線程調(diào)用会宪,即采用線程安全函數(shù)和libuv異步I/O工具庫這兩種策略,來優(yōu)化程序性能并保持流暢的用戶體驗蚯窥。
注意事項
以下將詳細闡述如何運用native方式創(chuàng)建子線程以執(zhí)行耗時任務(wù)掸鹅,并確保與JavaScript的無縫交互塞帐。為此,開發(fā)者可以利用 arkui_napi 倉庫提供的 NAPI(Node-API) 接口來實現(xiàn)跨語言調(diào)用的橋梁巍沙。該NAPI的設(shè)計嚴格遵循Node.js的NAPI規(guī)范葵姥,以便開發(fā)者能夠更輕松地理解和使用。
特別強調(diào)的是句携,JavaScript函數(shù)通常只能在主線程里調(diào)用榔幸。如果native側(cè)通過std::thread或pthread創(chuàng)建了子線程,那么napi_env矮嫉、napi_value以及napi_ref是不能直接在子線程上下文中使用的削咆。為確保正確性,當native端在子線程完成其計算或處理后蠢笋,若需要回調(diào)JavaScript函數(shù)拨齐,必須先通過線程同步機制將結(jié)果傳遞回主線程,然后才能安全地在主線程環(huán)境中調(diào)用JavaScript函數(shù)昨寞。
為解決這一問題瞻惋,以下將提出兩種有效的解決方案。
解決方案
線性安全函數(shù)
napi_threadsafe_function 提供了接口來創(chuàng)建一個可以在多線程間共享并安全使用的函數(shù)對象援岩。通過這個機制歼狼,子線程可以將數(shù)據(jù)傳遞給主線程,主線程接收到數(shù)據(jù)后會調(diào)用JavaScript回調(diào)函數(shù)進行處理窄俏。該接口包含用于創(chuàng)建蹂匹、銷毀線程安全函數(shù)以及在其之間發(fā)送消息和同步數(shù)據(jù)的方法碘菜。使用napi_threadsafe_function的一般步驟包括:
創(chuàng)建線程安全函數(shù): 通過調(diào)用napi_create_threadsafe_function()創(chuàng)建一個線程安全函數(shù)對象凹蜈。在此過程中,需要指定一個JavaScript回調(diào)函數(shù)忍啸,該函數(shù)將在主線程上執(zhí)行仰坦;同時設(shè)定相關(guān)的上下文信息,這個上下文可以在多個線程之間共享计雌,可以隨時通過調(diào)用napi_get_threadsafe_function_context()來獲取悄晃。此外,還可以選擇性地提供一個napi_finalize回調(diào)凿滤,用于在銷毀線程安全函數(shù)時執(zhí)行資源清理操作妈橄。
獲取使用權(quán): 在開始使用線程安全函數(shù)之前,調(diào)用napi_acquire_threadsafe_function()函數(shù)表明線程已準備就緒翁脆,可以開始對該線程安全函數(shù)進行操作眷蚓。
從子線程調(diào)用回調(diào): 在子線程中,通過調(diào)用napi_call_threadsafe_function()來異步觸發(fā)JavaScript回調(diào)函數(shù)反番,并將所需數(shù)據(jù)作為參數(shù)傳遞給該回調(diào)函數(shù)沙热。調(diào)用會被排隊叉钥,并最終在JavaScript主線程上執(zhí)行。
資源清理: 當線程安全函數(shù)不再需要時篙贸,應(yīng)當正確地釋放和清理與其關(guān)聯(lián)的資源投队。通常調(diào)用napi_release_threadsafe_function()函數(shù)來完成的,該函數(shù)會按照預(yù)定的策略處理尚未執(zhí)行完畢的回調(diào)爵川,并最終銷毀線程安全函數(shù)對象敷鸦。
延長生命周期
在JavaScript層面?zhèn)鬟f給native層的函數(shù)引用,其生命周期僅限于它所在的作用域內(nèi)寝贡。若要確保在超出該作用域后仍能繼續(xù)使用這個函數(shù)引用轧膘,需要采取適當?shù)姆椒▉硌娱L其生命周期。
可以通過調(diào)用napi_create_reference為JavaScript對象創(chuàng)建一個引用(reference)兔甘。這樣可以避免對象因垃圾回收機制而被提前釋放谎碍,從而有效地延長它的生命周期。然而洞焙,在創(chuàng)建引用之后蟆淀,務(wù)必牢記要在不再需要該引用時,調(diào)用napi_delete_reference來釋放引用澡匪,以防止內(nèi)存泄漏問題的發(fā)生熔任。
深入理解并妥善管理JavaScript與native接口之間對象的生命周期,對于編寫高效且無內(nèi)存泄漏隱患的代碼至關(guān)重要唁情。建議開發(fā)者進一步研究 生命周期管理 相關(guān)文檔和最佳實踐疑苔,以便更好地掌握。
libuv
libuv 是一個基于事件驅(qū)動的異步I/O庫甸鸟,對于耗時操作惦费,如果直接在libuv的主循環(huán)(event loop)中處理,會阻塞后續(xù)任務(wù)的執(zhí)行抢韭。為解決這個問題薪贫,libuv內(nèi)部維護了一個線程池,用于執(zhí)行一些耗時操作刻恭,并在這些操作完成后瞧省,將回調(diào)函數(shù)添加回主線程的event loop中等待執(zhí)行。
默認情況下鳍贾,libuv提供的線程池包含4個線程作為基本工作單元鞍匾,但最大線程數(shù)可以擴展到128個。通過預(yù)先設(shè)置環(huán)境變量 UV_THREADPOOL_SIZE 的值骑科,可以自定義線程池中的線程數(shù)量橡淑。當線程池初始化時,會創(chuàng)建相應(yīng)數(shù)量的工作線程纵散,并在每個線程內(nèi)部運行一個 uv_queue_work 函數(shù)梳码。
值得注意的是隐圾,libuv 中的線程池是全局共享資源,不論應(yīng)用中有多少個獨立的事件循環(huán)實例掰茶,它們都共用同一個線程池暇藏。這樣的設(shè)計旨在有效利用系統(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);
初始化一個工作請求盐碱,通過調(diào)用uv_queue_work函數(shù),可以安排指定的任務(wù)沪伙,在與事件循環(huán)(event loop)關(guān)聯(lián)的線程池中的一個線程上執(zhí)行瓮顽。一旦該任務(wù)(即work_cb回調(diào)函數(shù))完成其操作,將在事件循環(huán)線程中調(diào)用另一個回調(diào)函數(shù)after_work_cb围橡。
各參數(shù)的具體意義如下:
loop: 指向事件循環(huán)結(jié)構(gòu)體的指針暖混,所有異步操作都在這個事件循環(huán)上下文中進行管理。
req: 指向uv_work_t結(jié)構(gòu)體的指針翁授,用于傳遞給工作請求和回調(diào)函數(shù)的數(shù)據(jù)拣播。通常開發(fā)者會將自定義數(shù)據(jù)賦值給req->data成員變量以在回調(diào)中使用。
work_cb: 執(zhí)行實際工作的回調(diào)函數(shù)收擦,一些耗時的操作可以在此執(zhí)行贮配,該函數(shù)在線程池的一個線程上運行。
after_work_cb: 工作完成后在事件循環(huán)線程上調(diào)用的回調(diào)函數(shù)塞赂,常用于處理work_cb執(zhí)行結(jié)果或觸發(fā)進一步的JavaScript層面的操作泪勒。
需要注意的是,盡管uv_queue_work方法本身不直接涉及NAPI(Node-API)接口宴猾,但當涉及到與JavaScript線程交互時圆存,特別是從native層向JavaScript層傳遞數(shù)據(jù)并觸發(fā)回調(diào)時,需要正確地管理napi_value對象的生命周期鳍置。這需要合理使用napi_handle_scope和相關(guān)接口辽剧,來確保在JavaScript回調(diào)方法創(chuàng)建的napi_value對象送淆,在整個執(zhí)行過程中保持有效振坚,并在適當?shù)臅r候釋放資源傀蓉,以避免內(nèi)存泄漏問題。
示例代碼
下面的示例分別用線程安全函數(shù)和libuv實現(xiàn)了native的跨線程調(diào)用。該示例在ArkTS端傳入的JavaScript回調(diào)函數(shù)中對變量value進行加10運算帐姻,在native側(cè)開啟了3個子線程執(zhí)行業(yè)務(wù)邏輯,子線程業(yè)務(wù)邏輯完成之后回到主線程執(zhí)行ArkTS端傳入的JavaScript回調(diào)函數(shù)箱蝠,從而完成了對ArkTS端變量value的加30操作煤杀。
1.使用線程安全函數(shù)
ArkTS實現(xiàn)一個JavaScript回調(diào)函數(shù)。
參數(shù)為param谒出,函數(shù)體中對參數(shù)param加10后綁定變量value隅俘,并返回最新的param值邻奠。將回調(diào)函數(shù)作為參數(shù)調(diào)用native側(cè)的ThreadSafeTest接口。
// src/main/ets/pages/Index.ets
Button("threadSafeTest")
.width('40%')
.fontSize(20)
.onClick(()=> {
// native使用線程安全函數(shù)實現(xiàn)跨線程調(diào)用
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回調(diào)函數(shù)后通過napi_create_threadsafe_function創(chuàng)建一個線程安全函數(shù)tsfn碌宴,tsfn會回調(diào)主線程中的ThreadSafeCallJs,然后在ThreadSafeCallJs中調(diào)用ArkTS端傳入的JavaScript回調(diào)函數(shù)蒙畴。
// src/main/cpp/hello.cpp
napi_threadsafe_function tsfn; // 線程安全函數(shù)
static int g_cValue; // 保存value最新的值,作為參數(shù)傳給js回調(diào)函數(shù)
int g_threadNum = 3; // 線程數(shù)
struct CallbackContext {
napi_env env = nullptr;
napi_ref callbackRef = nullptr;
int retData = 0;
};
// 安全函數(shù)回調(diào)
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ù)調(diào)用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保存調(diào)用js后的返回結(jié)果
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ù)跨線程調(diào)用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);
// 使用安全線程跨線程調(diào)用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子線程中調(diào)用線程安全函數(shù)贰镣。
通過std::thread創(chuàng)建子線程,在子線程中通過napi_call_threadsafe_function調(diào)用線程安全函數(shù)tsfn膳凝,把CallbackContext 結(jié)構(gòu)體數(shù)據(jù)作為參數(shù)傳入ThreadSafeCallJs碑隆。這里在子線程中進行了簡單的業(yè)務(wù)處理,開發(fā)者可以根據(jù)自身實際需求進行相應(yīng)的業(yè)務(wù)操作蹬音。
// src/main/cpp/hello.cpp
// 在子線程中調(diào)用線程安全函數(shù)
for (int i = 0; i < g_threadNum; i++) {
// 創(chuàng)建回調(diào)參數(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è)務(wù)邏輯
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);
// 調(diào)用線程安全函數(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);
/* 以下直接在子線程中調(diào)用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回調(diào)函數(shù)上煤。
參數(shù)為param,函數(shù)體中對參數(shù)param加10后綁定變量value著淆,并返回最新的param值楼入。然后將回調(diào)函數(shù)作為參數(shù)調(diào)用native側(cè)的UvWorkTest接口。
// src/main/ets/pages/Index.ets
Button("libuvTest")
.width('40%')
.fontSize(20)
.onClick(()=> {
// native使用線程安全函數(shù)實現(xiàn)跨線程調(diào)用
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回調(diào)函數(shù)后創(chuàng)建子線程嘉熊,在子線程的執(zhí)行函數(shù)CallbackUvWorkTest中創(chuàng)建工作任務(wù)workReq,通過uv_queue_work將工作任務(wù)添加到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ù)結(jié)構(gòu),自定義數(shù)據(jù)結(jié)構(gòu)添加在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);
// 添加工作任務(wù)到libuv的隊列中
uv_queue_work(loop, workReq, WorkCallback, AfterWorkCallback);
}
// 使用uv_work callback 實現(xiàn)跨線程調(diào)用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è)務(wù)邏輯讲坎;after_work_cb位于主線程中孕惜,通過napi_call_function調(diào)用ArkTS端傳入的JavaScript回調(diào)函數(shù)。
// src/main/cpp/hello.cpp
void WorkCallback(uv_work_t *workReq)
{
// 另外一個子線程,一些耗時操作可以在此進行. 此處不能調(diào)用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í)行晨炕,可以在此調(diào)用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回調(diào)結(jié)果
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;
}
}
總結(jié)
線程安全函數(shù)和libuv方案都是在子線程的執(zhí)行函數(shù)運行結(jié)束后回到主線程衫画,并將JavaScript回調(diào)函數(shù)push到主線程的event-loop隊列里等待被執(zhí)行。
兩者的差異在于libuv的子線程屬于libuv線程池瓮栗,而線程安全函數(shù)的子線程需要根據(jù)業(yè)務(wù)要求自己創(chuàng)建削罩。另外在libuv中,JavaScript回調(diào)函數(shù)只能在子線程的主函數(shù)執(zhí)行完畢后被動被執(zhí)行费奸;而在線程安全函數(shù)中弥激,JavaScript回調(diào)函數(shù)則可以在任意線程中主動調(diào)用。
寫在最后
如果你覺得這篇內(nèi)容對你還蠻有幫助愿阐,我想邀請你幫我三個小忙:
- 點贊微服,轉(zhuǎn)發(fā),有你們的 『點贊和評論』缨历,才是我創(chuàng)造的動力以蕴。
- 關(guān)注小編糙麦,同時可以期待后續(xù)文章ing??,不定期分享原創(chuàng)知識丛肮。
- 想要獲取更多完整鴻蒙最新學(xué)習(xí)知識點喳资,請移步前往小編:
https://gitee.com/MNxiaona/733GH/blob/master/jianshu