🌐 Context Awareness
Node.js 历来作为单线程进程运行。这一切在 Node 10 引入 Worker Threads 后发生了变化。Worker Threads 添加了一种对 JavaScript 友好的并发抽象,原生插件开发者需要了解。这在实际中的意义是,你的原生插件可能会被加载和卸载多次,并且其代码可能在多个线程中并发执行。你必须采取特定步骤以确保你的原生插件代码能够正确运行。
🌐 Node.js has historically run as a single-threaded process. This all changed with the introduction of Worker Threads in Node 10. Worker Threads add a JavaScript-friendly concurrency abstraction that native add-on developers need to be aware of. What this means practically is that your native add-on may be loaded and unloaded more than once and its code may be executed concurrently in multiple threads. There are specific steps you must take to ensure your native add-on code runs correctly.
Worker 线程模型规定每个 Worker 完全独立运行,并使用父 Worker 提供的 MessagePort 对象与父线程通信。这使得 Worker 线程本质上彼此隔离。你的原生插件也是如此。
🌐 The Worker Thread model specifies that each Worker runs completely independently of each other and communicate to the parent Worker using a MessagePort object supplied by the parent. This makes the Worker Threads essentially isolated from one another. The same is true for your native add-on.
每个工作线程在其自身的环境中运行,该环境也被称为上下文。每个 Node-API 函数都可以使用该上下文作为 napi_env 值。
🌐 Each Worker Thread operates within its own environment which is also referred to as a context. The context is available to each Node-API function as an napi_env value.
🌐 Multiple loading and unloading
如果你的本地插件需要持久内存,将这块内存在静态全局空间中分配是灾难的根源。相反,_至关重要_的是,这块内存必须在每次本地插件初始化的上下文中分配。这块内存通常在你的本地插件的 Init 方法中分配。但在某些情况下,它也可以在你的本地插件运行时分配。
🌐 If your native add-on requires persistent memory, allocating this memory in static global space is a recipe for disaster. Instead, it is essential that this memory is allocated each time within the context in which the native add-on is initialized. This memory is typically allocated in your native add-on's Init method. But in some cases it can also be allocated as your native add-on is running.
Add-ons loaded in worker threads are unloaded when the last thread that has them loaded terminates (add-ons loaded on the main Node.js thread persist for the lifetime of the process.) To prevent memory leaks, any memory your native add-on has allocated must be freed when your native add-on is unloaded.
接下来的部分描述了两种不同的技术,你可以使用它们来分配和释放与本地插件关联的持久内存。这些技术可以单独使用,也可以在你的本地插件中一起使用。
🌐 The next sections describe two different techniques you can use to allocate and free persistent memory associated with your native add-on. The techniques may be used individually or together in your native add-on.
🌐 Instance data
Node-API 使你能够将本地插件分配的单个内存块与其运行的上下文关联。这种技术称为“实例数据”,当你的本地插件在加载时分配单个数据块时非常有用。
🌐 Node-API gives you the ability to associate a single piece of memory your native add-on allocates with the context under which it is running. This technique is called "instance data" and is useful when your native add-on allocates a single piece of data when its loaded.
napi_set_instance_data 允许你的原生插件将单个分配的内存与原生插件加载时的上下文关联起来。然后,可以在你的原生插件的任何地方调用 napi_get_instance_data 来检索已分配内存的位置。
🌐 The napi_set_instance_data allows your native add-on to associate a single piece of allocated memory with the context under which your native add-on is loaded. The napi_get_instance_data can then be called anywhere in you native add-on to retrieve the location of the memory that was allocated.
你在 napi_set_instance_data 调用中指定了一个终结器回调。终结器回调在你的本地插件从内存中释放时被调用,你应该在这里释放与此上下文关联的内存。
🌐 You specify a finalizer callback in your napi_set_instance_data call. The finalizer callback gets called when your native add-on is released from memory and is where you should release the memory associated with this context.
🌐 Resources
环境生命周期 API Node.js 文档,适用于 napi_set_instance_data 和 napi_get_instance_data。
🌐 Example
在这个例子中,创建了多个工作线程。每个工作线程都会创建一个与该工作线程的上下文关联的 AddonData 结构体,通过调用 napi_set_instance_data 实现。随着时间的推移,该结构体中保存的数值会通过一个计算成本较高的操作进行递增和递减。
🌐 In this example, a number of Worker Threads are created. Each Worker Thread creates an AddonData struct that is tied to the Worker Thread's context using a call to napi_set_instance_data. Over time, the value held in the struct is incremented and decremented using a computationally expensive operation.
随着时间的推移,工作线程完成它们的操作,此时在 DeleteAddonData 函数中释放分配的结构体。
🌐 In time, the Worker Threads complete their operations at which time the allocated struct is freed in the DeleteAddonData function.
#include <assert.h> #include <math.h> #include <stdlib.h> #define NAPI_EXPERIMENTAL #include <node_api.h> // Structure containing information needed for as long as the addon exists. It // replaces the use of global static data with per-addon-instance data by // associating an instance of this structure with each instance of this addon // during addon initialization. The instance of this structure is then passed to // each binding the addon provides. Thus, the data stored in an instance of this // structure is available to each binding, just as global static data would be. typedef struct { double value; } AddonData; // This is the actual, useful work performed: increment or decrement the value // stored per addon instance after passing it through a CPU-consuming but // otherwise useless calculation. static int ModifyAddonData(AddonData* data, double offset) { // Expensively increment or decrement the value. data->value = tan(atan(exp(log(sqrt(data->value * data->value))))) + offset; // Round the value to the nearest integer. data->value = (double)(((int)data->value) + (data->value - ((double)(int)data->value) > 0.5 ? 1 : 0)); // Return the value as an integer. return (int)(data->value); } // This is boilerplate. The instance of the `AddonData` structure created during // addon initialization must be destroyed when the addon is unloaded. static void DeleteAddonData(napi_env env, void* data, void* hint) { // Avoid unused parameter warnings. (void) env; (void) hint; // Free the per-addon-instance data. free(data); } // This is also boilerplate. It creates and initializes an instance of the // `AddonData` structure and ties its lifecycle to that of the addon instance's // `exports` object. This means that the data will be available to this instance // of the addon for as long as the JavaScript engine keeps it alive. 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; } // This function is called from JavaScript. It uses an expensive operation to // increment the value stored inside the `AddonData` structure by one. static napi_value Increment(napi_env env, napi_callback_info info) { // Retrieve the per-addon-instance data. AddonData* addon_data = NULL; assert(napi_get_instance_data(env, ((void**)&addon_data)) == napi_ok); // Increment the per-addon-instance value and create a new JavaScript integer // from it. napi_value result; assert(napi_create_int32(env, ModifyAddonData(addon_data, 1.0), &result) == napi_ok); // Return the JavaScript integer back to JavaScript. return result; } // This function is called from JavaScript. It uses an expensive operation to // decrement the value stored inside the `AddonData` structure by one. static napi_value Decrement(napi_env env, napi_callback_info info) { // Retrieve the per-addon-instance data. AddonData* addon_data = NULL; assert(napi_get_instance_data(env, ((void**)&addon_data)) == napi_ok); // Decrement the per-addon-instance value and create a new JavaScript integer // from it. napi_value result; assert(napi_create_int32(env, ModifyAddonData(addon_data, -1.0), &result) == napi_ok); // Return the JavaScript integer back to JavaScript. return result; } // Initialize the addon in such a way that it may be initialized multiple times // per process. The function body following this macro is provided the value // `env` which has type `napi_env` and the value `exports` which has type // `napi_value` and which refers to a JavaScript object that ultimately contains // the functions this addon wishes to expose. At the end, it must return a // `napi_value`. It may return `exports`, or it may create a new `napi_value` // and return that instead. NAPI_MODULE_INIT(/*env, exports*/) { // Create a new instance of the per-instance-data that will be associated with // the instance of the addon being initialized here and that will be destroyed // along with the instance of the addon. AddonData* addon_data = CreateAddonData(env, exports); // Declare the bindings this addon provides. The data created above is given // as the last initializer parameter, and will be given to the binding when it // is called. napi_property_descriptor bindings[] = { {"increment", NULL, Increment, NULL, NULL, NULL, napi_enumerable, addon_data}, {"decrement", NULL, Decrement, NULL, NULL, NULL, napi_enumerable, addon_data} }; // Expose the two bindings declared above to JavaScript. assert(napi_define_properties(env, exports, sizeof(bindings) / sizeof(bindings[0]), bindings) == napi_ok); // Return the `exports` object provided. It now has two new properties, which // are the functions we wish to expose to JavaScript. return exports; }
// Example illustrating the case where a native addon is loaded multiple times. // This entire file is executed twice, concurrently - once on the main thread, // and once on a thread launched from the main thread. // We load the worker threads module, which allows us to launch multiple Node.js // environments, each in its own thread. const { Worker, isMainThread } = require('worker_threads'); // We load the native addon. const addon = require('bindings')('multiple_load'); // The iteration count can be tweaked to ensure that the output from the two // threads is interleaved. Too few iterations and the output of one thread // follows the output of the other, not really illustrating the concurrency. const iterations = 1000; // This function is an idle loop that performs a random walk from 0 by calling // into the native addon to either increment or decrement the initial value. 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) { // On the main thread, we launch a worker and wait for it to come online. Then // we start the loop. new Worker(__filename).on('online', () => useAddon(addon, 'Main thread', iterations) ); } else { // On the secondary thread we immediately start the loop. useAddon(addon, 'Worker thread', iterations); }
🌐 Cleanup hooks
当你的本地插件运行的上下文被销毁时,你的本地插件可以从 Node.js 运行时引擎接收一个或多个通知。这为你的本地插件提供了在 Node.js 运行时引擎销毁上下文之前释放任何已分配内存的机会。
🌐 Your native add-on can receive one or more notifications from the Node.js runtime engine when the context in which your native-add-on has been running is being destroyed. This gives your native add-on the opportunity to release any allocated memory before the context is destroyed by the Node.js runtime engine.
这种技术的优点是你的本地插件可以分配多个内存块,以与本地插件运行时的上下文相关联。如果你在本地插件运行时需要从不同代码块分配多个内存缓冲区,这将非常有用。
🌐 The advantage of this technique is that your native add-on can allocate multiple pieces of memory to be associated with the context under which your native add-on is running. This can be useful if you need to allocate multiple memory buffers from different pieces of code as your native add-on is running.
缺点是,如果你需要访问这些分配的缓冲区,你需要在本地插件运行的上下文中自己负责跟踪指针。根据本地插件的架构,这可能是一个问题,也可能不是。
🌐 The drawback is that if you need to access these allocated buffer you are responsible for keeping track of the pointers yourself within the context your native add-on is running. Depending upon the architecture of your native add-on, this may or may not be an issue.
🌐 Resources
清理当前 Node.js 实例退出时 关于 napi_add_env_cleanup_hook 和 napi_remove_env_cleanup_hook 的 Node.js 文档。
🌐 Example
因为追踪已分配的缓冲区取决于本地插件的架构,这是一个简单的示例,展示了如何分配和释放缓冲区。
🌐 Because keeping track of the allocated buffers is dependent upon the architecture of the native add-on, this is a trivial example showing how the buffers can be allocated and released.
#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'; // We load the native addon. 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); });