企圖使用c++執行緒解決nodejs單執行緒問題

麦块程序猿發表於2024-06-25

企圖使用C++執行緒解決nodejs單執行緒效能問題時遇到的問題

首先我的C++程式碼如下

// AsyncThread.hpp
#include <napi.h>
#include <iostream>
#include <thread>
#include <chrono>

#include <vector>
#include <functional>
#include <queue>
#include <mutex>
#include <condition_variable>

#include "MyThreadPool.hpp"

using namespace std;

CppHelpper::MyThreadPool myThreadPool(5);
using Context = Napi::Reference<Napi::Value>;
void cppCallback(Napi::Env env, Napi::Function jsCallback, Context *context, void *any);
using TSFN = Napi::TypedThreadSafeFunction<Context, void, cppCallback>;
using FinalizerDataType = void;

void cppCallback(Napi::Env env, Napi::Function jsCallback, Context *context, void *info)
{
    jsCallback.Call({});
    if (info != NULL && info != nullptr)
    {
        delete info;
    }
}
// 處理非同步回撥
Napi::Value AsyncWork(const Napi::CallbackInfo &info)
{
    Napi::Env env = info.Env();

    // 當前回撥上下文
    Context *context = new Napi::Reference<Napi::Value>(Napi::Persistent(info.This()));
    Napi::Function jsAsyncCallbackFunction = info[0].As<Napi::Function>();
    // 建立一個TypedThreadSafeFunction
    TSFN tsfn = TSFN::New(
        env,
        jsAsyncCallbackFunction,   // JavaScript function called asynchronously
        "jsAsyncCallbackFunction", // Name
        0,                         // Unlimited queue
        1,                         // Only one thread will use this initially
        context,
        [](Napi::Env env, FinalizerDataType *finalizerDataType, Context *ctx) {
            delete ctx;                                                         // 手動釋放
        });

    myThreadPool.submitTask([tsfn]{
            // 執行js呼叫
            napi_status status = tsfn.BlockingCall();
            // napi_status status = tsfn.NonBlockingCall();
            if ( status != napi_ok ){
                cout<<"BlockingCall err: "+status<<endl;
            }

            // 釋放執行緒安全函式
            tsfn.Release(); 
    });

    return Napi::Boolean::New(env, true);
}


namespace AsyncThreadCall
{
    // 真正執行緒的非同步呼叫
    Napi::Value callAsyncThread(const Napi::CallbackInfo &info)
    {
        Napi::Env env = info.Env();
        if (info.Length() < 1 || !info[0].IsFunction())
        {
            env.RunScript("async worker is required:callAsyncThread(callback:()=>void)");
            return Napi::Boolean::New(info.Env(), false);
        }
        else
        {
            AsyncWork(info);
            return Napi::Boolean::New(info.Env(), true);
        }
    }

    // 程式退出切記清除執行緒池
    Napi::Value cleanThreadPool(const Napi::CallbackInfo &info)
    {
        myThreadPool.killAll();
        return Napi::Boolean::New(info.Env(), true);
    }
}

在這裡就不放出執行緒池的程式碼了,但測試執行緒池肯定沒問題的。上面程式碼簡單說一下是在幹什麼
透過將js的回撥函式傳入addon,在C++層面建立執行緒去執行js回撥,從此實現真正的多執行緒nodejs。
但理想很美好,現實很骨感,當我的測試程式碼如下時

const _Scanner2905_Addon_ = require("./Scanner2905.node");

_Scanner2905_Addon_.callAsyncThread(async () => {
    while(true){

    }
});
_Scanner2905_Addon_.callAsyncThread(async () => {
    for(let i=0; i<20; i++){
        console.log("(2)"+i);
    }
});
_Scanner2905_Addon_.callAsyncThread(async () => {
    for(let i=0; i<20; i++){
        console.log("(3)"+i);
    }
});

_Scanner2905_Addon_.cleanThreadPool();

講道理就算在上面的while(true)死迴圈,理論上不影響底下的迴圈正常執行,因為我在c++層面是透過多執行緒執行的,但實際上如果先執行了while(true),nodejs主執行緒依舊會卡住,底下的迴圈也不執行。why??
原本以為已經飛昇js到達cpp層面的我,不會再受到js的限制,沒想到nodejs的迴圈事件才是高手。即便我透過多執行緒呼叫了BlockCall(NonBlockCall)呼叫回撥,我依舊會受到迴圈事件影響。原因很簡單,我在cpp層面確實是真實的多執行緒,但是最後落到執行層面(也就是js回撥),他依舊會在迴圈事件裡,迴圈事件如果卡住,後續的所有nodejs執行也會卡住。
像不像你在公司,多個領導指揮你幹活一樣??他們以為很多領導發出多個命令,就能讓你也多執行緒執行一樣,但你就是你只有一個。同理,nodejs迴圈事件也就是單執行緒,再多的執行緒去call,他也是在單執行緒迴圈事件裡。
結論,如果真要實現在nodejs裡透過自己實現真正的多執行緒非同步,除非去深度修改迴圈事件,不然不可能實現(我真不想用官方的WorkerThreads,就好比為了實現子執行緒,我重新開一個v8環境執行js,那不是廢話嗎);換個思路,如果在nodejs真需要子執行緒的,可以把邏輯提煉出來交給c++去做,而不是再次把邏輯放到js執行層面。

相關文章