Node.js C++ AsyncWorker
Damit bei rechenintensiven Aufgaben der Event Loop nicht blockiert, kann man diese an den Worker Pool delegieren.
Im Artikel Node.js C++ Addon habe ich veranschaulicht wie man native Addons für Node.js entwickeln kann und darauf bau ich hier auf.
Node.js verwendet eine Event-Driven Architektur. Es hat einen Event Loop (Main Thread) für die Orchestrierung und x Worker in einen Worker Pool (Threadpool) für komplexere Prozesse.
Setup
Wie gehabt wechseln wir in den Projektordner und initialisieren das Modul. Danach wird node-addon-api, bindings und node-gyp installiert.
~/worker-addon$ npm int -y
~/worker-addon$ npm install node-addon-api bindings --save
~/worker-addon$ npm install node-gyp --save-dev
Die binding.gyp sieht wie folgt aus.
{
"targets": [
{
"target_name": "WorkerAddon",
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"sources": [ "src/worker.cc" ],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
'dependencies':
["<!(node -p \"require('node-addon-api').gyp\")"],
'defines':
[ 'NAPI_DISABLE_CPP_EXCEPTIONS' ]
}
]
}
Der C++ Source
Die worker.cc erstellen wir im Verzeichnis /worker-addon/src
#include "napi.h"
#include <chrono>
#include <thread>
using namespace Napi;
void doLongOperation(int s) {
std::this_thread::sleep_for(std::chrono::seconds(s));
}
Um eine Funktion mit langer Laufzeit zu simulieren, schicken wir den Thread einfach mittels std::this_thread::sleep_for
schlafen.
class Worker : public AsyncWorker {
public:
Worker(Function& callback, int& timeout)
: AsyncWorker(callback), seconds(timeout) {};
~Worker() {};
void Execute() {
doLongOperation(seconds);
};
void OnOK() {
HandleScope scope(Env());
Callback()
.Call({Env().Undefined(),
Number::New(Env(), (double) seconds)});
};
private:
int seconds;
};
Wir leiten aus der abstrakten Klasse Napi::AsyncWorker
die Subklasse Worker
ab. Das nimmt uns jede menge Arbeit mit dem Datenaustausch zwischen den Event Loop und Worker Threads ab.
Value DoStuffAsync(const CallbackInfo& info) {
Env env = info.Env();
if(info.Length() < 2) {
TypeError::New(env, "Wrong number of arguments")
.ThrowAsJavaScriptException();
return env.Null();
}
if(!info[0].IsNumber() || !info[1].IsFunction()) {
TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
return env.Null();
}
int timeout = info[0].As<Number>().Int32Value();
Function callback = info[1].As<Function>();
Worker* worker = new Worker(callback, timeout);
worker->Queue();
return env.Undefined();
}
Die DoStuffAsync
Funktion überprüft die übergebenen Argumente, konvertiert das erste Argument von Napi::Number
zu int
, instanziert einen Worker
und startet diesen.
Value DoStuff(const CallbackInfo& info) {
Env env = info.Env();
if(info.Length() != 1) {
TypeError::New(env, "Wrong number of arguments")
.ThrowAsJavaScriptException();
return env.Null();
}
if(!info[0].IsNumber()) {
TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
return env.Null();
}
int timeout = info[0].As<Number>().Int32Value();
doLongOperation(timeout);
return info[0].As<Number>();
}
Object Init(Env env, Object exports) {
exports["doStuffAsync"] =
Function::New(env, DoStuffAsync, std::string("DoStuffAsync"));
exports["doStuff"] =
Function::New(env, DoStuff, std::string("DoStuff"));
return exports;
}
NODE_API_MODULE(addon, Init)
Kompilieren des Moduls
~/worker-addon$ node-gyp configure
~/worker-addon$ node-gyp build
JavaScript
const Worker = require('bindings')('WorkerAddon');
setTimeout(() => {
console.log('Event')
}, 500)
setTimeout(() => {
console.log('Loop')
}, 1000)
setTimeout(() => {
console.log('blockiert\n')
}, 2000)
console.log(`finished after ${Worker.doStuff(4)}s`);
setTimeout(() => {
console.log('Event')
}, 500)
setTimeout(() => {
console.log('Loop')
}, 1000)
setTimeout(() => {
console.log('blockiert')
}, 2000)
setTimeout(() => {
console.log('nicht')
}, 3000)
Worker.doStuffAsync(4, (error, result) => {
console.log(`worker finished after ${result}s`);
});
~/worker-addon$ node worker.js
finished after 4s
Event
Loop
blockiert
Event
Loop
blockiert
nicht
worker finished after 4s