Исторически Node.js работал как однопоточный процесс. Всё изменилось с появлением Worker Threads в Node 10. Worker Threads добавляют дружелюбную к JavaScript абстракцию конкурентности, о которой разработчикам нативных аддонов нужно знать. На практике это означает, что ваш нативный аддон может загружаться и выгружаться более одного раза, а его код может выполняться конкурентно в нескольких потоках. Есть определённые шаги, которые вы обязаны предпринять, чтобы код вашего нативного аддона работал корректно.
Модель Worker Thread предписывает, что каждый Worker работает полностью независимо от остальных и общается с родительским Worker'ом через объект MessagePort, предоставляемый родителем. Это делает Worker Threads по сути изолированными друг от друга. То же верно и для вашего нативного аддона.
Каждый Worker Thread работает в своём собственном окружении, которое также называют контекстом. Контекст доступен каждой функции Node-API как значение napi_env.
Если вашему нативному аддону требуется постоянная память, выделять её в статическом глобальном пространстве — рецепт катастрофы. Вместо этого существенно важно, чтобы эта память выделялась каждый раз внутри контекста, в котором инициализируется нативный аддон. Обычно эта память выделяется в методе Init вашего нативного аддона. Но в некоторых случаях она может выделяться и во время работы нативного аддона.
Помимо описанной выше многократной загрузки, ваш нативный аддон также подвержен автоматической выгрузке сборщиком мусора движка JavaScript-среды выполнения, когда ваш нативный аддон больше не используется. Чтобы предотвратить утечки памяти, любая память, которую выделил ваш нативный аддон, должна быть освобождена при его выгрузке.
Следующие разделы описывают две разные техники, которые можно использовать для выделения и освобождения постоянной памяти, связанной с вашим нативным аддоном. Эти техники можно использовать по отдельности или вместе в вашем нативном аддоне.
Node-API даёт вам возможность связать один кусок памяти, который выделяет ваш нативный аддон, с контекстом, в котором он работает. Эта техника называется «instance data» и полезна, когда ваш нативный аддон выделяет единственный кусок данных при загрузке.
napi_set_instance_data позволяет вашему нативному аддону связать один выделенный кусок памяти с контекстом, в котором загружен ваш нативный аддон. napi_get_instance_data затем можно вызвать где угодно в вашем нативном аддоне, чтобы получить местоположение выделенной памяти.
Вы указываете колбэк-финализатор в вашем вызове napi_set_instance_data. Колбэк-финализатор вызывается, когда ваш нативный аддон освобождается из памяти, и именно в нём вам следует освободить память, связанную с этим контекстом.
Environment Life Cycle APIs — документация Node.js по napi_set_instance_data и napi_get_instance_data.
В этом примере создаётся несколько Worker Threads. Каждый Worker Thread создаёт структуру AddonData, привязанную к контексту Worker Thread через вызов napi_set_instance_data. Со временем значение в структуре увеличивается и уменьшается с помощью вычислительно затратной операции.
Со временем Worker Threads завершают свои операции, и тогда выделенная структура освобождается в функции DeleteAddonData.
#include <assert.h>
#include <math.h>
#include <stdlib.h>
#define NAPI_EXPERIMENTAL
#include <node_api.h>
// Структура, содержащая информацию, нужную на протяжении всего существования аддона. Она
// заменяет использование глобальных статических данных данными на каждый экземпляр аддона,
// связывая экземпляр этой структуры с каждым экземпляром этого аддона
// во время инициализации аддона. Затем экземпляр этой структуры передаётся
// каждому биндингу, который предоставляет аддон. Таким образом, данные, хранящиеся в экземпляре этой
// структуры, доступны каждому биндингу — так же, как были бы доступны глобальные статические данные.
typedef struct {
double value;
} AddonData;
// Это фактическая полезная работа: увеличить или уменьшить значение,
// хранящееся на каждый экземпляр аддона, пропустив его через нагружающее CPU, но
// в остальном бесполезное вычисление.
static int ModifyAddonData(AddonData* data, double offset) {
// Затратно увеличиваем или уменьшаем значение.
data->value = tan(atan(exp(log(sqrt(data->value * data->value))))) + offset;
// Округляем значение до ближайшего целого.
data->value =
(double)(((int)data->value) +
(data->value - ((double)(int)data->value) > 0.5 ? 1 : 0));
// Возвращаем значение как целое.
return (int)(data->value);
}
// Это шаблонный код. Экземпляр структуры `AddonData`, созданный во время
// инициализации аддона, должен быть уничтожен при выгрузке аддона. Эта
// функция будет вызвана, когда объект `exports` аддона будет собран сборщиком мусора.
static void DeleteAddonData(napi_env env, void* data, void* hint) {
// Избегаем предупреждений о неиспользуемых параметрах.
(void) env;
(void) hint;
// Освобождаем данные на каждый экземпляр аддона.
free(data);
}
// Это тоже шаблонный код. Он создаёт и инициализирует экземпляр структуры
// `AddonData` и привязывает его жизненный цикл к жизненному циклу объекта
// `exports` экземпляра аддона. Это значит, что данные будут доступны этому экземпляру
// аддона, пока движок JavaScript держит его «в живых».
static AddonData* CreateAddonData(napi_env env, napi_value exports) {
AddonData* result = malloc(sizeof(*result));
result->value = 0.0;
assert(napi_set_instance_data(env, result, DeleteAddonData, NULL) == napi_ok);
return result;
}
// Эта функция вызывается из JavaScript. Она использует затратную операцию, чтобы
// увеличить значение, хранящееся внутри структуры `AddonData`, на единицу.
static napi_value Increment(napi_env env, napi_callback_info info) {
// Извлекаем данные на каждый экземпляр аддона.
AddonData* addon_data = NULL;
assert(napi_get_instance_data(env, ((void**)&addon_data)) == napi_ok);
// Увеличиваем значение на каждый экземпляр аддона и создаём из него новое целое JavaScript.
napi_value result;
assert(napi_create_int32(env,
ModifyAddonData(addon_data, 1.0),
&result) == napi_ok);
// Возвращаем целое JavaScript обратно в JavaScript.
return result;
}
// Эта функция вызывается из JavaScript. Она использует затратную операцию, чтобы
// уменьшить значение, хранящееся внутри структуры `AddonData`, на единицу.
static napi_value Decrement(napi_env env, napi_callback_info info) {
// Извлекаем данные на каждый экземпляр аддона.
AddonData* addon_data = NULL;
assert(napi_get_instance_data(env, ((void**)&addon_data)) == napi_ok);
// Уменьшаем значение на каждый экземпляр аддона и создаём из него новое целое JavaScript.
napi_value result;
assert(napi_create_int32(env,
ModifyAddonData(addon_data, -1.0),
&result) == napi_ok);
// Возвращаем целое JavaScript обратно в JavaScript.
return result;
}
// Инициализируем аддон так, чтобы он мог инициализироваться несколько раз
// на процесс. Телу функции после этого макроса предоставляется значение
// `env` типа `napi_env` и значение `exports` типа
// `napi_value`, которое ссылается на JavaScript-объект, в конечном счёте содержащий
// функции, которые этот аддон хочет предоставить. В конце он обязан вернуть
// `napi_value`. Он может вернуть `exports` либо создать новое `napi_value`
// и вернуть его вместо этого.
NAPI_MODULE_INIT(/*env, exports*/) {
// Создаём новый экземпляр данных на каждый экземпляр, который будет связан с
// инициализируемым здесь экземпляром аддона и который будет уничтожен
// вместе с экземпляром аддона.
AddonData* addon_data = CreateAddonData(env, exports);
// Объявляем биндинги, которые предоставляет этот аддон. Созданные выше данные передаются
// как последний параметр инициализатора и будут переданы биндингу при его
// вызове.
napi_property_descriptor bindings[] = {
{"increment", NULL, Increment, NULL, NULL, NULL, napi_enumerable, addon_data},
{"decrement", NULL, Decrement, NULL, NULL, NULL, napi_enumerable, addon_data}
};
// Предоставляем два объявленных выше биндинга в JavaScript.
assert(napi_define_properties(env,
exports,
sizeof(bindings) / sizeof(bindings[0]),
bindings) == napi_ok);
// Возвращаем предоставленный объект `exports`. Теперь у него два новых свойства —
// это функции, которые мы хотим предоставить в JavaScript.
return exports;
}// Пример, иллюстрирующий случай, когда нативный аддон загружается несколько раз.
// Весь этот файл выполняется дважды, конкурентно — один раз в главном потоке
// и один раз в потоке, запущенном из главного потока.
// Загружаем модуль worker threads, который позволяет нам запускать несколько окружений Node.js,
// каждое в своём потоке.
const { Worker, isMainThread } = require('worker_threads');
// Загружаем нативный аддон.
const addon = require('bindings')('multiple_load');
// Число итераций можно подкрутить, чтобы вывод из двух
// потоков чередовался. Слишком мало итераций — и вывод одного потока
// следует за выводом другого, не иллюстрируя конкурентность.
const iterations = 1000;
// Эта функция — холостой цикл, выполняющий случайное блуждание от 0, вызывая
// нативный аддон, чтобы либо увеличить, либо уменьшить исходное значение.
function useAddon(addon, prefix, iterations) {
if (iterations >= 0) {
if (Math.random() < 0.5) {
console.log(prefix + ': new value (decremented): ' + addon.decrement());
} else {
console.log(prefix + ': new value (incremented): ' + addon.increment());
}
setImmediate(() => useAddon(addon, prefix, --iterations));
}
}
if (isMainThread) {
// В главном потоке мы запускаем worker и ждём, пока он выйдет онлайн. Затем
// запускаем цикл.
new Worker(__filename).on('online', () =>
useAddon(addon, 'Main thread', iterations)
);
} else {
// Во вторичном потоке мы сразу запускаем цикл.
useAddon(addon, 'Worker thread', iterations);
}Ваш нативный аддон может получать одно или несколько уведомлений от движка среды выполнения Node.js, когда контекст, в котором работал ваш нативный аддон, уничтожается. Это даёт вашему нативному аддону возможность освободить любую выделенную память до того, как контекст будет уничтожен движком среды выполнения Node.js.
Преимущество этой техники в том, что ваш нативный аддон может выделять несколько кусков памяти, связанных с контекстом, в котором он работает. Это может быть полезно, если вам нужно выделять несколько буферов памяти из разных участков кода во время работы вашего нативного аддона.
Недостаток в том, что если вам нужно обращаться к этим выделенным буферам, вы сами отвечаете за отслеживание указателей внутри контекста, в котором работает ваш нативный аддон. В зависимости от архитектуры вашего нативного аддона, это может быть или не быть проблемой.
Cleanup on exit of the current Node.js instance — документация Node.js по napi_add_env_cleanup_hook и napi_remove_env_cleanup_hook.
Поскольку отслеживание выделенных буферов зависит от архитектуры нативного аддона, это тривиальный пример, показывающий, как буферы можно выделять и освобождать.
#include <stdlib.h>
#include <stdio.h>
#include "node_api.h"
namespace {
void CleanupHook (void* arg) {
printf("cleanup(%d)\n", *static_cast<int*>(arg));
free(arg);
}
napi_value Init(napi_env env, napi_value exports) {
for (int i = 1; i < 5; i++) {
int* value = (int*)malloc(sizeof(*value));
*value = i;
napi_add_env_cleanup_hook(env, CleanupHook, value);
}
return exports;
}
} // anonymous namespace
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)'use strict';
// Загружаем нативный аддон.
const addon = require('bindings')('multiple_load');
const assert = require('assert');
const child_process = require('child_process');
assert.ok(addon);
if (process.argv[2] === 'child') {
const childAddon = require('bindings')('multiple_load');
assert.ok(childAddon);
process.exit(0);
}
const child = child_process.fork(__filename, ['child'], {
stdio: 'inherit',
});
child.on('exit', code => {
assert.strictEqual(code, 0);
});