为什么我写的 Nodejs C++ Addons 善后这么卡?
为什么我写的 Nodejs C++ Addons 善后这么卡?
https://github.com/XadillaX/thmclrx/tree/fb1fc3e69c8fa7dac7037ba86e923ce0c8da9ab1/src
这个就是我写的 Addon,以及在
https://github.com/XadillaX/thmclrx/blob/fb1fc3e69c8fa7dac7037ba86e923ce0c8da9ab1/test/test.js
是测试代码。
我指的慢不是执行的时候慢,而是“善后”,我也不知道怎么解释——
总之效果就是 console
已经有东西打印出来了,但是要过好一段时间之后才能开始下一步操作。
比如把
for(var i = 0; i < 35500; i++) {
rgb.push({
r : Number.random(0, 255),
g : Number.random(0, 255),
b : Number.random(0, 255)
});
}
里面的 35500 改成一个很小的数字,那么处理完 console
出来之后秒退,但是如果改成更大的话,console
之后要等非常就之后程序才结束。
照理说都已经 console
出来了,说明由 C++ 处理的那段程序段已经结束了,但是为什么还是要等那么久呢?
在线等!急!
这个问题可能是由于你的C++ Addon在处理完任务后没有正确地释放资源或完成异步操作导致的。在Node.js中使用C++ Addon时,如果你的任务是异步的(例如涉及文件I/O、网络请求或其他耗时操作),你需要确保这些操作完成后正确地回调到JavaScript环境。
以下是一个简单的示例,展示如何在C++ Addon中实现异步操作,并确保在操作完成后调用回调函数。
示例代码
假设你有一个需要处理大量数据的C++ Addon,你可以使用Nan::AsyncWorker
来管理异步操作。
C++ Addon (addon.cc)
#include <nan.h>
class MyAsyncWorker : public Nan::AsyncWorker {
public:
MyAsyncWorker(Nan::Callback *callback)
: AsyncWorker(callback), m_result(0) {}
~MyAsyncWorker() {}
void Execute() override {
// 模拟耗时操作
for(int i = 0; i < 35500; i++) {
m_result += i;
}
}
void Destroy() override {}
void HandleOKCallback() override {
v8::Local<v8::Value> argv[] = { Nan::New(m_result).ToLocalChecked() };
callback->Call(1, argv);
}
private:
int m_result;
};
NAN_METHOD(MyFunction) {
Nan::HandleScope scope;
if(info.Length() < 1 || !info[0]->IsFunction()) {
return Nan::ThrowError("Callback is required.");
}
Nan::Callback *callback = new Nan::Callback(info[0].As<v8::Function>());
Nan::AsyncQueueWorker(new MyAsyncWorker(callback));
}
NODE_MODULE(addon, MyFunction)
测试代码 (test.js)
const addon = require("./build/Release/addon");
addon.MyFunction((result) => {
console.log(`Result: ${result}`);
});
解释
- 异步工作类 (
MyAsyncWorker
): 继承自Nan::AsyncWorker
,用于处理耗时操作。Execute
方法中进行实际的计算。 - 异步调用 (
MyFunction
): 使用Nan::AsyncQueueWorker
将MyAsyncWorker
添加到事件循环队列中,这样不会阻塞主线程。 - 回调 (
HandleOKCallback
): 当异步操作完成后,调用HandleOKCallback
方法将结果传递回JavaScript环境。
通过这种方式,即使你的C++ Addon中包含长时间运行的操作,也不会阻塞Node.js的事件循环,从而避免了程序结束后等待很长时间的情况。
因为gc时间到了,js主线程必须卡住 :D
你改用Buffer传参数,全部测完再释放,就知道区别了
实际上node.js程序只要遇到大数组、大JSON的计算,都会很卡,没有例外的。计算方面的事情,我就不建议把数据保存在js这一侧。如果全部是在native代码里存储和计算,情况会好得多。需要数据的时候,再包装成js对象返给js线程就行了,一次不要返太多数据,免得序列化的时候又卡住了
传rgb数组到native的时候,会再复制一次这个超大的数组,你说怎么不会有gc
我认为Buffer不会,如果这个也要复制内存块的话,那就太对不起这个名字了。没测过,我之前遇到这类问题后,就果断把数据结构全部在Addon里实现了,js这边并不拥有任何大数组什么的
另一个处理方式是使用stream之类的机制来做大量数据的传递。无论如何,不要把一个很大的对象传给 C++ addon,大对象必然会卡住js线程。
感觉还是不对啊,我做了个实验:
C++ 文件
#include <v8.h>
#include <node.h>
using namespace v8;
Handle<Value> Test1(const Arguments& args)
{
HandleScope scope;
Local<Value> arg = args[0];
Local<Array> arr = Local<Array>::Cast(arg);
return scope.Close(arr);
}
void Init(Handle<Object> exports)
{
exports->Set(String::NewSymbol(“test1”),
FunctionTemplate::New(Test1)->GetFunction());
}
NODE_MODULE(test, Init);
Node 文件
var tester = require("./build/Release/test.node");
var object1 = [];
var object2;
for(var i = 0; i < 1000000; i++) {
object1.push({
r: i,
g: i,
b: i
});
}
console.log(tester.test1(object1)[0]);
console.log(tester.test1(object1)[0]);
这里的两次调用 tester.test1
几乎无间隔,而且跑完马上就退出了,没有很明显的所谓 GC 的感觉。
不知何解?
好吧问题我自己找到了。
在 C++
层面上写内存池为了贪省力直接用了 stl
。用什么不好,而且我还用了 list
,然后就卡住了。
现在还是贪省力,不过改用了 queue
然后就解决了。
从你的描述来看,问题可能出在C++ Addons的资源释放或内存管理上。在Node.js中,C++ Addons是通过libuv的线程池来执行异步操作的。如果C++代码没有正确地完成资源释放或者同步工作,可能会导致主线程被阻塞,从而导致程序“善后”时出现延迟。
示例代码分析
假设你的C++ Addon中有一个函数generateRGB
用于生成RGB颜色数组,并且你在JavaScript中调用这个函数:
// addon.cpp
#include <nan.h>
NAN_METHOD(generateRGB) {
int count = info[0]->Uint32Value();
std::vector<nan::TypedValue> rgb;
for (int i = 0; i < count; i++) {
double r = Nan::Random();
double g = Nan::Random();
double b = Nan::Random();
rgb.push_back(Nan::New<v8::Object>());
rgb.back()->Set(Nan::New("r").ToLocalChecked(), Nan::New(r));
rgb.back()->Set(Nan::New("g").ToLocalChecked(), Nan::New(g));
rgb.back()->Set(Nan::New("b").ToLocalChecked(), Nan::New(b));
}
// 将结果返回给JavaScript
info.GetReturnValue().Set(Nan::New(rgb));
}
NODE_MODULE(addon, generateRGB)
JavaScript 测试代码
// test.js
const addon = require("./build/Release/addon");
console.time("process");
addon.generateRGB(35500).then(rgb => {
console.log("RGB generated:", rgb);
console.timeEnd("process");
});
解决方案
- 检查资源释放:确保C++代码中的所有资源(如文件句柄、网络连接等)在使用完毕后都被正确关闭。
- 避免长时间阻塞:如果C++代码中有耗时的操作,考虑将其放入单独的线程中执行,以避免阻塞主线程。
- 优化内存管理:如果你在C++代码中分配了大量的内存,确保及时释放这些内存。
具体示例
在C++代码中,可以使用uv_queue_work
来将耗时任务放到后台线程中执行,这样不会阻塞主线程:
// addon.cpp
#include <nan.h>
#include <uv.h>
void GenerateRGBAsync(uv_work_t* req) {
// 在这里执行生成RGB数组的逻辑
}
void GenerateRGBAfter(uv_work_t* req) {
Nan::HandleScope scope;
v8::Local<v8::Array> result = Nan::New<v8::Array>();
// 将结果填充到result中
Nan::SetCallback(Nan::GetCurrentContext()->Global(), Nan::New("processCompleted").ToLocalChecked(), Nan::New(result));
}
NAN_METHOD(generateRGB) {
uv_work_t* req = new uv_work_t();
req->data = nullptr;
uv_queue_work(uv_default_loop(), req, GenerateRGBAsync, GenerateRGBAfter);
}
NODE_MODULE(addon, generateRGB)
这样,生成RGB数组的操作将在后台线程中执行,不会阻塞主线程。