Node.js And C++__7.流和事件

本文详细介绍了如何在Node.js和C++之间构建事件分发和流接口,涉及从C++分发事件、构建C++插件、创建事件分发接口、JavaScript适配器、通用Event-based插件等步骤。通过实例展示了如何实现流媒体接口,包括使用异步执行、对象封装和线程安全队列,以及如何从C++向JavaScript发送JSON数据。文章还探讨了如何将事件发射器转换为流,以及如何从JavaScript向C++发送事件和数据。
摘要由CSDN通过智能技术生成

我们已经走了很长一段路了!我们不仅学会了如何使用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<
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值