Node.jsC++

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.

worker-addon/binding.gyp
{
"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

worker-addon/src/worker.cc
#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.

worker-addon/src/worker.cc
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.

worker-addon/src/worker.cc
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.

worker-addon/src/worker.cc
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

worker-addon/worker.js
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