我们已经走了很长一段路了!我们不仅学会了如何使用V8处理基本Node.js与C++集成,但是我们现在已经掌握了对象封装和状态维护的处理办法。我们知道如何使用nan来抽象异步执行和减少对V8的版本依赖。现在是时候把所有这些都放在一起,开发一个集成的综合性插件了,它提供了更复杂的方法,可以从JavaScript中与C++进行交互。在本章中,我们将构建添加了模拟EventEmitter和streams的接口的插件。这些接口特别适合于长期运行的C++任务的线程,这些任务不断地将输入发送回JavaScript或从其接收一系列输入。
在本章中讨论的概念是高级的,如果您只是浏览了最后几章,您可能想要回过头来仔细阅读它们。为了获得我们的流媒体接口,我们将大量使用nan、异步执行,甚至对象封装(ObjectWrap)。我将通过一系列不同的示例来介绍这些概念,然后将这些代码融合在一起,最后得到一个可重用的SDK,用于开发流C++和addons。
作为第一步,我们将着重于通过一个EventEmitter来显示C++插件,而不是函数调用。我们将从那里构建,将这些事件发分发封装在JS适配器模块中,以公开流媒体接口。
如果您顺着了nodecpp-demo的代码阅读,请转到该存储库中的流目录,查看本章完整的、已完成的代码。
从C++ 分发事件
有时我们使用C++来完成一些繁重的计算任务,这些任务会随着时间的推移产生部分结果。像这样使用addons的一种方法是使它异步,当结果完成时,我们可以使addon返回整个结果(可能包括部分结果)。或者,我们可以实现异步的addon,通过调用JS回调来立即返回所有的部分结果。更进一步来说,捕获数据序列的“Node way”通常是通过 EventEmitter
接口进行的。
consuming:很明显,质因数分解可能不是一个足够强大的计算任务,它需要构建一个完整的C++addon,让我们沿着一个流媒体界面。然而,就像本书中的大多数例子一样,这是一个简单的问题,它不需要太多的解释,却可以很好地展示编程模型。
一个适合(某种程度)的问题是质因数分解。分解一个N,特别是当它很大的时候,有点耗费时间[1]。然而,我们计算的是一系列的因子,而不是全部。让我们试着开发一个C++addon,在计算它们的时候释放出一些因子,让我们得到一个Node.js程序看起来是这样的:
[1]需要说明的是,质因数分解很可能不是一个足够大的计算任务,需要构建一个完整的C++addon,让它沿着一个流媒体界面。然而,就像本书中的大多数例子一样,它是一个简单的问题,它不需要大量的解释——但是很好地演示了编程模型。
// factorization.js
var factor = require(<path to addon>);
const factorization = factor({n:9007199254740991});
factorizer.on('factor', function(factor){
console.log("Factor: " + factor);
});
factorizer.on('close', function() {
console.log("Factorization complete");
});
输出:
Factor: 6361
Factor: 69431
Factor: 20394401
Factorization complete
正如您所看到的,这个假设的addon会发出factor类型的事件,以及标准的close事件(同样的error
事件也是发起的!)让我们构建它。
构建C++插件
C++addon代码的核心是分解过程。在我们深入了解addon的样板文件和线程安全之前,我们可以对C++代码的真实面貌进行建模:
void factorize(uint32_t n) {
while (n%2 == 0) {
writeToNode(2);
n = n/2;
}
for (uint32_t i = 3; i <= n; i = i+2) {
while (n%i == 0) {
writeToNode(i);
n = n/i;
}
}
这里的关键是,send_to_node 方法必须从线程边界获得整数因子,从工作线程到事件循环线程,以及最终被映射到一个适当的event emitter。注意,send_to_node 本质上是“发送进度”问题——我们有一个长时间运行的addon,并发送频繁的更新。正如我们在第6章中所看到的,对于这样的和addon,AsyncProgressWorker,nan为我们提供了一个很好的基础操作类,我们将在很大程度上利用这个类,并进行一些重要的更改,以确保我们的addon接收到我们发送的每一条消息。
[2]如果没有快速的JavaScript处理, AsyncProgressWorker
会崩溃持续的进度更新。例如,如果在下一次事件循环周期之前,在工人代码中报告的进度为0,50,100%,则是 Node.js只会收到一个单一的进度指标——100%。从进度报告的角度来看,这很有意义——但不是为了传递消息!
如果没有JavaScript快速处理,AsyncProgressWorker就会崩溃。例如,如果在下一个事件循环周期之前,在所有的工作线程代码中都有0、50、100%的进度报告,那么 Node.js只会收到一个单一的进度指示。从进度报告的角度来看,这是很有意义的——但不是消息传递!
class Factorizer : public AsyncProgressWorker {
public:
Factorizer(Callback *progress, Callback *callback, uint32_t n)
: AsyncProgressWorker(callback), progress(progress), n(n)
{}
~Factorizer() {}
// Executes in the new worker thread (background)
void Execute (
const AsyncProgressWorker::ExecutionProgress& progress) {
uint32_t factor = 2;
while (n%2 == 0) {
progress.Send(
reinterpret_cast<const char*>(&factor),
sizeof(uint32_t));
n = n/2;
}
for (uint32_t i = 3; i <= n; i = i+2) {
while (n%i == 0) {
progress.Send(
reinterpret_cast<const char*>(&i),
sizeof(uint32_t));
n = n/i;
}
}
}
// Executes in the event-loop thread
void HandleProgressCallback(const char *data, size_t size) {
HandleScope scope;
v8::Local<v8::Value> argv[] = {
New<v8::Integer>(
*reinterpret_cast<int*>(const_cast<char*>(data)))
};
progress->Call(1, argv);
}
protected:
Callback *progress;
uint32_t n;
};
NAN_METHOD(Factor) {
Callback *progress = new Callback(info[1].As<v8::Function>());
Callback *callback = new Callback(info[2].As<v8::Function>());
AsyncQueueWorker(new Factorizer( callback, progress
, To<uint32_t>(info[0]).FromJust()
));
}
NAN_MODULE_INIT(Init) {
Set(target
, New<v8::String>("factorize").ToLocalChecked()
, New<v8::FunctionTemplate>(Factor)->GetFunction());
}
NODE_MODULE(factorization, Init)
上面的代码创建了addon,现在我们可以使用它了(假设这个js文件和cpp/gyp文件在同一个目录中)。
"use strict";
const path = require("path");
var addon_path = path.join(__dirname, "build/Release/factorization");
const worker = require(addon_path);
worker.factorize(9007199254740991, function() {
console.log("Factorization Complete");
}, function(factor){
console.log("Factor: " + factor);
})
不幸的是,如果您在一台相当快的机器上运行它,您将得到一个相当令人失望的输出!
Factorization complete
这里的问题是分解实际上是非常快的。如果深入到AsyncProgressWorker中,您会发现,如果已经完成了工作,代码实际上会拒绝调用进度回调。在这种情况下,我们已经在 lib_uv
事件循环之前完成了循环,甚至有机会运行,因此没有报告任何进展。作为一个快速解决方案,让我们在 Execute
函数的最末尾处放置一个 Sleep(1000)
——这应该足够让事件循环唤醒并处理我们的更新:
void Execute (
const AsyncProgressWorker::ExecutionProgress& progress) {
uint32_t factor = 2;
while (n%2 == 0) {
progress.Send(
reinterpret_cast<const char*>(&factor),
sizeof(uint32_t));
n = n/2;
}
for (uint32_t i = 3; i <= n; i = i+2) {
while (n%i == 0) {
progress.Send(
reinterpret_cast<const char*>(&i),
sizeof(uint32_t));
n = n/i;
}
}
}
不过,我们的产出可能不会好得多:
Factor: 65537
Factorization complete
其他因素——比如,3、5、17和257——发生了什么变化?同样, AsyncProgressWorker
的实现又回到了我们这里,因为在一个事件循环周期内没有任何功能来处理多个“进度”更新。我们最终取得了进展。在事件循环出现之前,发送5次机会来处理我们的请求——每次都重写“当前”进程,所以前4次已经被覆盖。只有最后一个值被传播和接受。
我们可以通过重新实现 AsyncProgressWorker
解决这一问题,而是让我们建立一个队列来保存每个我们发送消息,然后确保每次对 HandleProgressCallback
调用回调(至少一次,如果工作线程没有已经完成的)我们排的队列,发送每一项通过个人进度回调JavaScript调用。注意,我将使用一个线程安全的队列实现——而不是标准的C++队列。这是至关重要的,因为实际上我们所创建的是一个生产者-消费者队列模型,其中“progress”更新由后台线程生成,并在HandleProgressCallback
(事件循环线程)中消费。
这是队列,也将在其余的示例中使用,所以我已经对它进行了模板化。我们现在只使用readAll
,但在后面的例子中,我们还会使用 read
t来从队列中获取一个项目。它使用C++11同步原语来确保线程安全。
template<typename Data>
class PCQueue
{
public:
void write(Data data) {
while (true) {
std::unique_lock<std::mutex> locker(mu);
buffer_.push_back(data);
locker.unlock();
cond.notify_all();
return;
}
}
Data read() {
while (true)
{
std::unique_lock<std::mutex> locker(mu);
cond.wait(locker, [this](){
return buffer_.size() > 0;
});
Data back = buffer_.front();
buffer_.pop_front();
locker.unlock();
cond.notify_all();
return back;
}
}
void readAll(std::deque<Data> & target) {
std::unique_lock<std::mutex> locker(mu);
std::copy(
buffer_.begin(),
buffer_.end(),
std::back_inserter(target));
buffer_.clear();
locker.unlock();
}
PCQueue() {}
private:
std::mutex mu;
std::condition_variable cond;
std::deque<Data> buffer_;
};
现在,让我们将其中一个队列放入到我们的worker类中(命名为 toNode
,类型为 PCQueue<uint32_t>
),并调整回调,以便使用队列来存储进度消息。首先,让我们创建一个私有函数drainQueue
,它读取所有项,并通过调用所提供的回调函数将它们发送到Node。
// private method within the Factorizer class
void drainQueue() {
HandleScope scope;
// drain the queue - since we might only get
// called once for many writes
std::deque<uint32_t> contents;
toNode.readAll(contents);
for(uint32_t & item : contents) {
v8::Local<v8::Value> argv[] = {
New<v8::Integer>(*reinterpret_cast<int*>(&item))
};
progress->Call(1, argv);
}
}
现在我们可以在HandleProgressCallback
方法中使用这个函数,也可以在HandleOKCallback中使用;这 AsyncProgressWorker中的一个虚拟函数,我们现在将重写这个函数。
void HandleOKCallback() {
drainQueue();
callback->Call(0, NULL);
}
void HandleProgressCallback(const char *data, size_t size) {
drainQueue();
}
现在让我们把我们的因子放到这个队列上。让我们创建一个包装器函数,它接受progress
对象和uin32_t
数据项,并将数据添加到队列中,并触发一个进度事件。注意,队列是在 progress.Send
中发送的。实际上,我们可以发送任何东西——我们实际上忽略了发送到这个函数的内容,因为这些调用将导致调用 drainQueue
。我将把代码清理留给读者——如果我们愿意打乱异步的实际实现,我们就可以做出一些明显的简化。
// protected member of Factorizer
void writeToNode(
const AsyncProgressWorker::ExecutionProgress& progress,
uint32_t & factor){
toNode.write(factor);
progress.Send(
reinterpret_cast<