AsyncWorker
Introduction
You may have a project in which you have a piece of long-running C/C++ code that you want run in the background instead of on Node’s main event loop. Node-API’s AsyncWorker
class is designed specifically for this case.
As a programmer, your job is essentially to subclass AsyncWorker
and to implement the Execute
method. You’ll also probably implement a wrapper function to make using your AsyncWorker
easier.
In this example, we’re going to create a SimpleAsyncWorker
class that subclasses AsyncWorker
. The worker will take an integer value indicating the length of time it is to “work.” When the worker completes, it will return a text string indicating how long it worked. In one case, the worker will indicate an error instead.
SimpleAsyncWorker
Here’s the C++ header file for SimpleAsyncWorker
:
#pragma once
#include <napi.h>
using namespace Napi;
class SimpleAsyncWorker : public AsyncWorker {
public:
SimpleAsyncWorker(Function& callback, int runTime);
virtual ~SimpleAsyncWorker(){};
void Execute();
void OnOK();
private:
int runTime;
};
This code declares a constructor that takes as an argument the length of time (in seconds) the SimpleAsyncWorker
is to run. A private data member is declared to hold this value.
The header also declares two methods, Execute
and OnOK
, which override methods declared by AsyncWorker
, and are described in more detail below.
And here’s the C++ implementation file for SimpleAsyncWorker
:
#include "SimpleAsyncWorker.h"
#include <chrono>
#include <thread>
SimpleAsyncWorker::SimpleAsyncWorker(Function& callback, int runTime)
: AsyncWorker(callback), runTime(runTime){};
void SimpleAsyncWorker::Execute() {
std::this_thread::sleep_for(std::chrono::seconds(runTime));
if (runTime == 4) SetError("Oops! Failed after 'working' 4 seconds.");
};
void SimpleAsyncWorker::OnOK() {
std::string msg = "SimpleAsyncWorker returning after 'working' " +
std::to_string(runTime) + " seconds.";
Callback().Call({Env().Null(), String::New(Env(), msg)});
};
The constructor takes two arguments. callback
is the JavaScript function that gets called when the Execute
method returns. callback
gets called whether there was an error or not. The second constructor argument, runTime
, is an integer value indicating how long (in seconds) the worker is to run.
Node will run the code of the Execute
method in a thread separate from the thread running Node’s main event loop. The Execute
method has no access to any part of the Node-API environment. For this reason, the runTime
input value was stored by the constructor as a private data member.
In this implementation, the Execute
method simply waits the number of seconds specified earlier by runTime
. This is where the long running code goes in a real implementation. To demonstrate how error handling works, this Execute
method declares an error when requested to run 4 seconds.
The OnOK
method is called after the Execute
method returns unless the Execute
method calls SetError
or in the case where C++ exceptions are enabled and an exception is thrown. In the case of an error, the OnError
method is called instead of OnOK
. The default OnError
implementation simply calls the AsyncWorker
callback function with the error as the only argument.
In this implementation, the OnOK
method formulates a string value and passes it as the second argument to the callback
function specified in the constructor. The first argument passed to the callback
function is a JavaScript null
value. The reason for this is that a single callback function is called whether an error occurs or not. The default OnError
method, which SimpleAsyncWorker
does not override, passes the error as the first argument to the callback. This will become more clear in the next section.
Note that unlike Execute
, the OnOK
and OnError
methods do have access to the Node-API environment.
RunSimpleAsyncWorker
We need a C++ function that instantiates SimpleAsyncWorker
objects and requests them to be queued. This function needs to be registered with Node-API so that it is accessible from the JavaScript code.
#include "SimpleAsyncWorker.h"
Value runSimpleAsyncWorker(const CallbackInfo& info) {
int runTime = info[0].As<Number>();
Function callback = info[1].As<Function>();
SimpleAsyncWorker* asyncWorker = new SimpleAsyncWorker(callback, runTime);
asyncWorker->Queue();
std::string msg =
"SimpleAsyncWorker for " + std::to_string(runTime) + " seconds queued.";
return String::New(info.Env(), msg.c_str());
};
Object Init(Env env, Object exports) {
exports["runSimpleAsyncWorker"] = Function::New(
env, runSimpleAsyncWorker, std::string("runSimpleAsyncWorker"));
return exports;
}
NODE_API_MODULE(addon, Init)
The runSimpleAsyncWorker
function, which is accessible from JavaScript, takes two arguments which are passed through the info
argument. The first argument, which is passed as info[0]
, is the runTime
and the second argument is the JavaScript callback function which gets called when the Execute
method returns.
The code then instantiates a SimpleAsyncWorker
object and requests that it be queued for possible execution on the next tick. Unless the SimpleAsyncWorker
object is queued, its Execute
method will never be called.
Once the SimpleAsyncWorker
object is queued, runSimpleAsyncWorker
formulates a text string and returns it to the caller.
Running in JavaScript
Here’s a simple JavaScript program that shows how to run SimpleAsyncWorker
instances.
const runWorker = require('../build/Release/napi-asyncworker-example-native');
let result = runWorker.runSimpleAsyncWorker(2, AsyncWorkerCompletion);
console.log("runSimpleAsyncWorker returned '"+result+"'.");
result = runWorker.runSimpleAsyncWorker(4, AsyncWorkerCompletion);
console.log("runSimpleAsyncWorker returned '"+result+"'.");
result = runWorker.runSimpleAsyncWorker(8, AsyncWorkerCompletion);
console.log("runSimpleAsyncWorker returned '"+result+"'.");
function AsyncWorkerCompletion (err, result) {
if (err) {
console.log("SimpleAsyncWorker returned an error: ", err);
} else {
console.log("SimpleAsyncWorker returned '"+result+"'.");
}
};
In this code, the runSimpleAsyncWorker
function is called three times, each with a different runTime
parameter. Each call specifies AsyncWorkerCompletion
as the callback function.
The AsyncWorkerCompletion
function is coded to handle the cases where the Execute
method reports an error and when it does not. It simply logs to the console when it’s called.
Here’s what the output looks like when the JavaScript successfully runs:
runSimpleAsyncWorker returned 'SimpleAsyncWorker for 2 seconds queued.'.
runSimpleAsyncWorker returned 'SimpleAsyncWorker for 4 seconds queued.'.
runSimpleAsyncWorker returned 'SimpleAsyncWorker for 8 seconds queued.'.
SimpleAsyncWorker returned 'SimpleAsyncWorker returning after 'working' 2 seconds.'.
SimpleAsyncWorker returned an error: [Error: Oops! Failed after 'working' 4 seconds.]
SimpleAsyncWorker returned 'SimpleAsyncWorker returning after 'working' 8 seconds.'.
As expected, each call to runSimpleAsyncWorker
immediately returns. The AsyncWorkerCompletion
function gets called when each SimpleAsyncWorker
completes.
Caveats
-
It is absolutely essential that the
Execute
method makes no Node-API calls. This means that theExecute
method has no access to any input values passed by the JavaScript code.Typically, the
AsyncWorker
class constructor will collect the information it needs from the JavaScript objects and store copies of that information as data members. The results of theExecute
method can then be turned back into JavaScript objects in theOnOK
method. - The Node process is aware of all running
Execute
methods and will not terminate until all runningExecute
methods have returned. - An AsyncWorker can be safely terminated with a call to
AsyncWorker::Cancel
from the main thread.
Resources
AsyncWorker Class Documentation.
The complete source and build files for this project can be found at inspiredware/napi-asyncworker-example.