异步编程解决方案

事件发布/订阅模式

事件监听器模式是一种广泛用于异步编程的模式,是回调函数的事件化,又称发布/订阅模式。

// 订阅
emitter.on("event1", function (message) {
  console.log(message); 
});
// 发布
emitter.emit('event1', "I am message!");

事件发布/订阅模式可以实现一个事件与多个回调函数的关联,这些回调函数又称为事件侦听器。通过emit()发布事件后,消息会立即传递给当前事件的所有侦听器执行,侦听器可以很灵活地添加和删除,使得事件和具体处理逻辑之间可以很轻松地关联和解耦。因为事件发布者无须关注订阅的侦听器如何实现业务逻辑,甚至不用关注有多少个侦听器存在,数据通过消息的方式可以很灵活的传递。

事件发布/订阅模式自身并无同步和异步调用的问题,但在Node中,emit()调用多半是伴随事件循环而异步触发的,所以我们说事件发布/订阅广泛应用于异步编程。

事件侦听器模式也是一种钩子(hook)机制,利用钩子导出内部数据或状态给外部的调用者,Node中的很多对象具有黑盒的特点,功能点较少,如果不通过事件钩子的形式,我们就无法获取对象在运行期间的中间值或内部状态。这样,可以使编程者不用关注组件是如何启动和执行的,只需关注在需要的事件点上即可。

例如HTTP请求的代码中,程序员只需要将视线放在error、data、end这些业务事件点上即可,至于内部的流程如何,无需过于关注。

值得一提的是,Node对事件发布/订阅的机制做了一些额外的处理,这大多是基于健壮性而考虑的,下面为两个具体的细节点。

1、如果对一个事件添加了超过10个侦听器,将会得到一条警告,因为设计者认为侦听器太多可能导致内存泄露,也可能存在过多占用CPU的场景。调用emitter.setMaxListeners(0);可以将这个限制去掉。

2、为了处理异常,EventEmitter对象对error事件进行了特殊对待。如果运行期间的错误触发了error事件,EventEmitter会检查是否有对error事件添加过侦听器,如果添加了,这个错误将会交由改侦听器处理,否则这个错误将作为异常抛出,如果外部没有捕获这个异常,将会引起线程退出。

继承events模块

在Node中,开发者可以轻松的继承EventEmitter类,利用事件机制来解决业务问题。

var events = require('events');

function Stream() {
  events.EventEmitter.call(this);
}

util.inherits(Stream, events.EventEmitter);

// util.inherits封装了继承的方法

利用事件队列解决雪崩问题

在计算机中,缓存由于存放在内存中,访问速度十分快,常常用于加速数据访问,让绝大多数的请求不必重复去做一些低效的数据读取,所谓雪崩问题,就是在高访问量、大并发量的情况下缓存失效的情景,此时大量的请求同时涌入数据库中,数据库无法同时承受如此大的查询请求,导致崩溃。

雪崩的过程:

1、redis集群彻底崩溃

2、缓存服务大量对redis的请求hang住,占用资源

3、缓存服务大量的请求打到源头服务去查询mysql,直接打死mysql

4、源头服务因为mysql被打死也崩溃,对源服务的请求也hang住,占用资源

5、缓存服务大量的资源全部耗费在访问redis和源服务无果,最后自己被拖死,无法提供服务

6、nginx无法访问缓存服务,redis和源服务,只能基于本地缓存提供服务,但是缓存过期后,没有数据提供

7、网站崩溃

var events = require('events');

var proxy = new events.EventEmitter(); 
var status = "ready";
var select = function (callback) {
  proxy.once("selected", callback);
  if (status === "ready") {
    status = "pending";
    db.select("SQL", function (results) {
      proxy.emit("selected", results);
      status = "ready";
    });
  }
};

当进行多次同一SQL操作的时候,所有的回调在一个查询周期中(ready - pending - ready)都会被压入事件队列中,等待执行,并且利用了once()确保了所有回调都只执行一次后被移除,(因为监听的都是selected事件,在多次emit的时候会多次触发回调)。当查询结束后,调用emit触发selected事件,执行事件队列中所有相关回调。这种方式节省了重复的数据库调用产生的开销。

多异步之间的协作方案

一般而言,事件与侦听器的关系是一对多,但在异步编程中,也会出现事件与侦听器的关系是多对一的,也就是说一个业务逻辑可能依赖多个事件回调的结果。

场景:渲染页面需要模板读取、数据读取、本地化资源读取这三步,得到三种数据进行最终渲染,且这三种操作互不依赖。

var count = 0;
var results = {};
var done = function(key, value) {
  results[key] = value;
  count++;
  if(count === 3) {
    // 渲染页面
    render(results)
  }
}

fs.readFile(template_path, "utf8", function(err, template){
  done("template", template)
})

db.query(sql, function(err, data){
  done("data", data)
})

l10n.get(function(err, data){
  done("resources", resources)
})

因为三个操作互不依赖,当count = 3的时候,说明三个操作都成功完成,得到想要的数据开始渲染页面,一般会把这个用于检测次数的变量叫做哨兵变量

在多对多的场景中,可以使用发布/订阅的方式来完成一对多的发散。

// 使用偏函数完成 多对一的收敛
var after = function(times, callback) {
  var count = 0;
  var results = {};
  return function(key, value){
    results[key] = value;
    count++;
    if(count === times) {
      // 渲染页面
      callback(results)
    }
  }
}

// 使用发布/订阅的方式来完成一对多的发散
var emitter = new events.EventEmitter(); 
var done = after(3, render);
emitter.on("done", done); // 用于渲染
emitter.on("done", other); // 统一获取相同数据的操作,用于别的用途

fs.readFile(template_path, "utf8", function (err, template) { 
  emitter.emit("done", "template", template);
});

db.query(sql, function (err, data) {
  emitter.emit("done", "data", data); 
});

l10n.get(function (err, resources) { 
  emitter.emit("done", "resources", resources);
});

Promise/ Deferred 模式

在异步调用中,回调总是需要被预先设定,所以出现了Promise/ Deferred 模式来实现先执行异步调用,延迟传递回调。

1、可以对一个事件传入多个回调

2、写法优雅,一定程度的缓解了嵌套过深的问题。

$.get('/api', {
  success: onSuccess,
  error: onError
})

// 变迁为
$.get('/api')
.success(onSuccess)
.success(onSuccess2)
.error(onError)

使用events模块的简单实现

const EventEmitter = require('events').EventEmitter;

util.inherits(Promise, EventEmitter);

Promise.prototype.then = function (fulfilledHandler, errorHandler, progressHandler) { 
  if (typeof fulfilledHandler === 'function') {
    this.once('success', fulfilledHandler); 
  }
  if (typeof errorHandler === 'function') {
    this.once('error', errorHandler);
  }
  if (typeof progressHandler === 'function') {
    this.on('progress', progressHandler); 
  }
  return this; 
};

var Deferred = function () { 
  this.state = 'unfulfilled'; 
  this.promise = new Promise();
};

Deferred.prototype.resolve = function (obj) { 
  this.state = 'fulfilled'; 
  this.promise.emit('success', obj);
};

Deferred.prototype.reject = function (err) { 
  this.state = 'failed'; 
  this.promise.emit('error', err);
};

Deferred.prototype.progress = function (data) { 
  this.promise.emit('progress', data);
};

var promisify = function (res) { 
  var deferred = new Deferred(); 
  var result = '';
  res.on('data', function (chunk) {
    result += chunk;
    deferred.progress(chunk); 
  });

  res.on('end', function () { 
    deferred.resolve(result);
  });

  res.on('error', function (err) {
    deferred.reject(err); 
  });
  return deferred.promise; 
};

// 调用

promisify(res).then(function () { 
  // Done
}, function (err) { 
  // Error
}, function (chunk) {
  // progress
  console.log('BODY: ' + chunk);
});

Deferred主要是用于内部, 用于维护异步模型的状态,Promise则作用于外部,通过then()方法暴露给外部以添加自定义逻辑。

多异步协作

在ES6中Promise的实现中,是使用Promise.all()这个方法实现,它接受一个promise实例组成的数组作为参数,使用一个新的Promise包裹promise的循环调用操作,当所有promise实例调用完成时,resolve这个新的Promise,期间如果发生错误就reject这个新的Promise。

链式调用

1、将所有的回调都存到队列中。

2、Promise完成时,逐个执行回调,一旦检测到执行回调返回了新的Promise时,停止执行,调用其的then方法并将队列中余下的回调转交给它。

相关逻辑都在then方法中实现

{{% notice info %}}

关于Promise的具体实现参考:一个Promise实现

ES6-Promise源码

{{% /notice %}}

流程控制

除了事件和Promise外, 还有一类方法是需要手工调用才能持续执行后续调用的,我们将此类方法叫做尾触发,常见的关键词是next。

ES6中Generator函数就是采用这类方法来控制流程,同时最新的async await 相关API更是将写法变得更加方便。

{{% notice info %}}

参考:
Generator 函数的异步应用、
async 函数、
异步流程控制

{{% /notice %}}

事件发布/订阅模式相对算是一种较为原始的方式,Promise/Deferred模式贡献了一个非常不错的异步任务模型的抽象,而异步流程控制方案与Promise/Deferred模式的思路不同,Promise/Deferred的重头在于封装异步的调用部分,流程控制则显得没有模式,将处理的重点放置在回调函数的注入上,从自由度来讲,流程控制相对灵活得多。

你可能感兴趣的:(异步编程解决方案)