NodeJs C/C++ Addons 非阻塞多線程回調(diào)

系列文章

  1. C/C++ Addons 入門 Hello world!
  2. C/C++ Addons 對象參數(shù)及回調(diào)函數(shù)
  3. C/C++ Addons 非阻塞多線程回調(diào)
  4. C/C++ Addons windows 下 .dll 動態(tài)鏈接庫 實用篇

完整代碼

  • 這篇文章是解決上一篇文章碰到的 Sleep() 后線程 阻塞(休眠) 問題探索的解決方案

node.js 異步理解 (參考深入淺出 node.js)

  • 簡單的交代下:
    所謂 node.js 異步事件環(huán),底層使用了類似 while(true) 的方式開了一個死循環(huán),然后在每次循環(huán)的時候去異步隊列中取出一個或多個待執(zhí)行的任務(wù)執(zhí)行。
    待執(zhí)行任務(wù)來自 node.js 提供的異步接口如:setTimeoutfshttp 模塊提供的異步方法等,這里不一一列出。
    任何任務(wù)都需要線程去執(zhí)行,既然主線程不執(zhí)行他們那么這些非阻塞的任務(wù)是被怎樣執(zhí)行的呢?其實本質(zhì)上 node.js 底層還是使用了多線程
    當(dāng)然 windows*nix 平臺實現(xiàn)略有不同,windows 底層由操作系統(tǒng)接管, *nix 底層是 node.js 自己實現(xiàn)的多線程任務(wù)池,這里不展開細(xì)講。
非阻塞

代碼實現(xiàn)

  • C/C++ src/thread-cb.c
#include <stdio.h>
#include <windows.h>
#include <assert.h>
// 開啟 N-API 實驗性功能,如:多線程 napi_threadsafe_function
#define NAPI_EXPERIMENTAL
#include <node_api.h>

typedef struct{
    napi_async_work work;          // 保存線程任務(wù)的
    napi_threadsafe_function tsfn; // 保存回調(diào)函數(shù)的
}Addon;

/** 調(diào)試報錯用的 */
static void catch_err(napi_env env, napi_status status) {
    if (status != napi_ok) {
        const napi_extended_error_info* error_info = NULL;
        napi_get_last_error_info(env, &error_info);
        printf("%s\n", error_info->error_message);
        exit(0);
    }
}

/** 調(diào)用 js-callback 用的 */
static void call_js(napi_env env, napi_value js_cb, void* context, void* data) {
    (void)context; // 用不到它
    (void)data;    // 用不到它
    
    if (env != NULL) {
        napi_value undefined, js_prime;

        napi_get_undefined(env, &undefined); // 創(chuàng)建一個 js 的 undefined
        catch_err(env, napi_call_function(env,
            undefined, // js 回調(diào)的 this 對象
            js_cb,     // js 回調(diào)函數(shù)句柄
            0,         // js 回調(diào)函數(shù)接受參數(shù)個數(shù)
            NULL,      // js 回調(diào)函數(shù)參數(shù)數(shù)組
            NULL));    // js 回調(diào)函數(shù)中如果有 retrun,將會被 result 接受到,NULL 代表忽略
    }
}

/** 執(zhí)行線程 */
static void execute_work(napi_env env, void* data) {
    Addon* addon = (Addon*)data;

    // 拿到 js-callback 函數(shù)
    catch_err(env, napi_acquire_threadsafe_function(addon->tsfn));

    // 延遲四秒執(zhí)行
    Sleep(4000);

    // 調(diào)用 js-callback 函數(shù)
    catch_err(env, napi_call_threadsafe_function(
        addon->tsfn,          // js-callback 函數(shù)
        NULL,                 // call_js 的第四個參數(shù)
        napi_tsfn_blocking)); // 阻塞模式調(diào)用

    // 釋放句柄
    catch_err(env, napi_release_threadsafe_function(addon->tsfn, napi_tsfn_release));
}

/** 線程執(zhí)行完成 */
static void work_complete(napi_env env, napi_status status, void* data) {
    Addon* addon = (Addon*)data;

    // 釋放句柄
    catch_err(env, napi_release_threadsafe_function(addon->tsfn, napi_tsfn_release));

    // 回收任務(wù)
    catch_err(env, napi_delete_async_work(env, addon->work));

    addon->work = NULL;
    addon->tsfn = NULL;
}

/**
 * start_thread 啟動線程
 * 關(guān)于 static 關(guān)鍵字用不用都行的,官方的例子有用到 static
 * 以我 js 的能力我猜的可能是開多個線程下,可以公用一個函數(shù),節(jié)約內(nèi)存開銷 (歡迎大神來討論 ??)
 */
static napi_value start_thread(napi_env env, napi_callback_info info) {
    size_t argc = 1;       // js 傳進來的參數(shù)個數(shù)
    napi_value js_cb;      // js 傳進來的回調(diào)函數(shù)
    napi_value work_name;  // 給線程起個名字
    Addon* addon;          // “實例化” 結(jié)構(gòu)體 (個人理解是取出了傳進來的 js-cabllback 地址指針,期待大神來討論 ??)
    napi_status sts;       // 程序執(zhí)行狀態(tài)
    
    sts = napi_get_cb_info(
        env,               // 執(zhí)行上下文,可以理解為 js 那個 “事件環(huán)”
        info,              // 上下文信息
        &argc,             // 收到參數(shù)的個數(shù)
        &js_cb,            // 接收 js 參數(shù)
        NULL,              // 接收 js 的 this 對象
        (void**)(&addon)   // 取得 js 傳進來的 callback 的指針地址
    );
    catch_err(env, sts);

    // 打醬油的 ^_^
    assert(addon->work == NULL && "Only one work item must exist at a time");

    // 創(chuàng)建線程名字
    catch_err(env, napi_create_string_utf8(env,
        "N-API Thread-safe Call from Async Work Item",
        NAPI_AUTO_LENGTH,
        &work_name));

    // 把 js function 變成任意線程都可以執(zhí)行的函數(shù)
    // 醬紫我們就可以在開出來的子線程中調(diào)用它咯
    sts = napi_create_threadsafe_function(env,

        // 其他線程的 js 函數(shù)
        // call_js 的第二個參數(shù)
        // 也就是我們 addon.start(function) 傳進來的 function
        js_cb,

        // 可能傳遞給一些異步任務(wù)async_hooks鉤子傳遞初始化數(shù)據(jù) (期待大神討論 ??)
        // 個人理解 N-API 中的 async 指的就是多線程任務(wù)
        // 一個線程任務(wù),在 N-API 中由 async work 調(diào)用
        NULL,

        work_name,       // 給線程起個名字,給 async_hooks 鉤子提供一個標(biāo)識符
        0,               // (官網(wǎng)直譯)最大線程隊列數(shù)量,0 代表沒限制
        1,               // (官網(wǎng)直譯)初始化線程數(shù)量,其中包括主線程
        NULL,            // (官網(wǎng)直譯)線程之間可以傳遞數(shù)據(jù)(官網(wǎng)直譯)
        NULL,            // (官網(wǎng)直譯)線程之間可以傳遞函數(shù),函數(shù)注銷時候被調(diào)用
        NULL,            // (官網(wǎng)直譯)附加給函數(shù)的執(zhí)行上下文,應(yīng)該就是 
        call_js,         // call_js 的第三個參數(shù)
        &(addon->tsfn)); // js 傳進來的函數(shù),可以理解為真實的 js 函數(shù)所在內(nèi)存地址 (期待大神討論 ??)
    catch_err(env, sts);

    // 負(fù)責(zé)執(zhí)行上面創(chuàng)建的函數(shù)
    sts = napi_create_async_work(env,
        NULL,            // 可能傳遞 async_hooks 一些初始化數(shù)據(jù)
        work_name,       // 給線程起個名字,給 async_hooks 鉤子提供一個標(biāo)識符
        execute_work,    // 線程執(zhí)行時候執(zhí)行的函數(shù) (與主線程并行執(zhí)行)
        work_complete,   // 線程執(zhí)行完時候的回調(diào)
        addon,           // 既 execute_work、work_complete 中的 void* data
        &(addon->work)); // 線程句柄
    catch_err(env, sts);

    // 將線程放到待執(zhí)行隊列中
    sts = napi_queue_async_work(env,
        // 要執(zhí)行線程的句柄
        addon->work);
    catch_err(env, sts);

    return NULL; // 這個貌似是返回給 js-callback 的返回值
}

napi_value init(napi_env env, napi_value exports) {
    // 這里等價于 const obj = new Object();
    // 這回知道面向?qū)ο笫钦淼牧税???
    // 類的本質(zhì)就是“結(jié)構(gòu)體”演化而來的,new(開辟堆內(nèi)存空間) 關(guān)鍵字是 malloc(申請內(nèi)存空間) 變種過來的
    Addon* addon = (Addon*)malloc(sizeof(*addon));

    // 等價于 obj.work = null;
    addon->work = NULL;

    // 個人 js 水平有限,Object 類研究的不深
    // 可以說,精通 Object 類的小伙伴可以自己想想咯,反正給對象掛一個屬性、函數(shù)需要的東東,都在這里了
    // 相等于 const fun = () => {}, attr = 'Hello';
    napi_property_descriptor desc = {
        "start",       // 屬性名稱
        NULL,          // -- 沒想明白
        start_thread,  // 函數(shù)體
        NULL,          // 屬性 getter
        NULL,          // 屬性 setter
        NULL,          // 屬性描述符
        napi_default,
        addon          // (官網(wǎng)直譯)也可以寫 NULL,調(diào)用 getter 時候返回的數(shù)據(jù)
    };
    // 相當(dāng)于 const obj = { fun, attr };
    napi_define_properties(env, exports, 1, &desc); // 將屬性掛載到 exports 上面
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, init)


// 配合另一種導(dǎo)出模塊用的
//static void addon_getting_unloaded(napi_env env, void* data, void* hint) {
//  Addon* addon = (Addon*)data;
//  assert(addon->work == NULL && "No work in progress at module unload");
//  free(addon);
//}

// 另一種導(dǎo)出模塊的宏定義(有空再研究??)
//NAPI_MODULE_INIT() {
//  Addon* addon = (Addon*)malloc(sizeof(*addon));
//  addon->work = NULL;
//
//  napi_property_descriptor desc = {
//      "start",
//      NULL,
//      start_thread,
//      NULL,
//      NULL,
//      NULL,
//      napi_default,
//      addon
//  };
//
//  napi_define_properties(env, exports, 1, &desc);
//  napi_wrap(env, exports, addon, addon_getting_unloaded, NULL, NULL);
//  // return exports; // 可寫可不寫
//}

  • javascript test/.js
const addon = require('../build/Release/thread_cb.node');

let second = 0;
let cb_exucted = false;

addon.start(function() {
  cb_exucted = true;
  console.log(`Asynchronous callback exectued.`);
});

const t = setInterval(() => {
  if (cb_exucted) {
    clearTimeout(t);
  }
  console.log(`After ${++second} seconds.`);
}, 1000);
  • 運行
$ node test/thread-cb.js
After 1 seconds.
After 2 seconds.
After 3 seconds.
Asynchronous callback exectued.
After 4 seconds.

OK! 木有任何問題,我們親愛的 js 還是你熟悉的那個非阻塞模式運行 ??

寫在最后:
雖然這是教程的第三篇,但這篇是最難的了,我英語又不好看官方 N-API 文檔好費勁的 ??
其次個人覺得吧現(xiàn)在地表上面的操作系統(tǒng)就 windwos*nix 兩款了,還都是 C 語言打造的,所以好多時候你再厲害的語言(java,php,python...)底層和 C 及 C 造的操作系統(tǒng)都有著千絲萬縷的聯(lián)系
所以說,雖然個人作為一個前端er,還是很推薦了解下 C 及 C 的產(chǎn)物操作系統(tǒng),其實操作系統(tǒng)可以簡單的理解是用 C 寫的 工具集合 幫你操作音頻、視頻、網(wǎng)絡(luò)、磁盤、合理調(diào)度 CPU、合適開辟內(nèi)存等等 ??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內(nèi)容