异步编程

238 阅读3分钟

异步编程

一.函数式编程

函数式编程回调函数 深层嵌套)是异步编程的基础。
函数式编程的本质是:++函数是一等公民,可以调用,也可以作为 参数/返回值++

1.高阶函数

任何将函数作为参数/返回值函数都可以称为高阶函数
回调高阶函数形成了一种后续传递的风格,将业务重点放在了回调函数中。
典型的回调有:

  • Array.sort(callback)
    不同的callback可以得到不同的顺序结果。
var points = [40, 100, 1, 5, 25, 10];
points.sort(function(a, b) {
  return a - b;
});
// [ 1, 5, 10, 25, 40, 100 ]

同样的还有reducesomeevery等等。

  • 事件注册
var emitter = new events.EventEmitter();
emitter.on('event_foo', function () {
  // TODO
});

同一个事件event_foo,可以执行不同的业务逻辑。

2.偏函数用法

偏函数指一个由参数/变量已经预置的函数返回生成的函数。

  • 案例一
var toString = Object.prototype.toString;

var isString = function (obj) {
  return toString.call(obj) == '[object String]';
};
var isFunction = function (obj) {
  return toString.call(obj) == '[object Function]';
};

仅仅一个判断类型的功能,不同的类型却需要不同的函数定义,较为冗余

var isType = function (type) {
  return function (obj) {
    return toString.call(obj) == '[object ' + type + ']';
  };
};

var isString = isType('String');
var isFunction = isType('Function');

isType就是那个包装函数,执行时预置了type变量,结果返回一个偏函数

  • 案例二
_.after = function(times, func) {
  if (times <= 0) return func();
  return function() {
    if (--times < 1) { return func.apply(this, arguments); }
  };
};

after包装函数返回的偏函数,会虚假执行times次后,才会真正执行。

二.异步编程优势与难点

1.优势

基于事件驱动的非阻塞IO模型

2.难点

异常捕获

  • 作为调用者,异步方法执行,结果立即返回,回调会存放起来,直到下一轮事件循环来调用,所以在当前事件循环捕获错误没有意义。
// 这个捕获无意义
try {
  async(callback);
} catch (e) {
  // TODO
}
  • 作为异步方法提供者,需要:
    • 确保调用者的回调会被执行
    • 提供异常结果给调用者自行判断
var async = function (callback) {
  process.nextTick(function() {
    var results = something;
    if (error) {
      return callback(error);
    }
    callback(null, results);
  });
};

嵌套过深

多个异步嵌套调用的场景

fs.readdir(path.join(__dirname, '..'), function (err, files) {
  files.forEach(function (filename, index) {
    fs.readFile(filename, 'utf8', function (err, file) {
      // TODO
    });
  });
});

阻塞代码

缺少线程沉睡能力,而setTimeout()并不能阻止后续代码执行,同步的阻塞会阻塞CPU,而非线程沉睡

多线程编程

充分利用多核CPU:

  • 浏览器前端有web workers

    • 充分利用多核CPU,减少阻塞UI渲染
    • 浏览器支持程度不够
    • 无法提高UI渲染效率
  • Node后端

    • child_process基础API
    • cluster模块深层次应用

异步转同步

偶尔的同步应用场景因缺少API而尴尬

三.异步编程解决方案

1.事件发布订阅模式

Node的events模块提供了一个发布订阅的简单实现,并且大部分模块都继承了该模块。
事件的回调函数称为侦听器侦听器很方便的实现业务逻辑事件之间的关联和耦合。

案例

var options = {
  host: 'www.google.com',
  port: 80,
  path: '/upload',
  method: 'POST'
};
var req = http.request(options, function (res) {
  console.log('STATUS: ' + res.statusCode);
  console.log('HEADERS: ' + JSON.stringify(res.headers));
  res.setEncoding('utf8');
  res.on('data', function (chunk) {
    console.log('BODY: ' + chunk);
  });
  res.on('end', function () {
    // TODO
  });
});
req.on('error', function (e) {
  console.log('problem with request: ' + e.message);
});
// write data to request body
req.write('data\n');
req.write('data\n');
req.end();
  • 发布订阅多应用于异步编程,这是因为事件的发布(触发),往往是++由事件循环异步触发++

data end事件都是在异步请求得到响应时触发的。

  • 解耦业务逻辑

调用方只需要关心data end error的侦听器函数

  • 钩子(hook)机制,钩子导出内部状态和数据

调用方通过data end error事件侦听器函数来获取异步请求的中间状态数据

继承events模块
var events = require('events');
 
function Stream() {
  events.EventEmitter.call(this);
}
util.inherits(Stream, events.EventEmitter);

构造类Stream实现对events模块的继承

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

雪崩问题指高访问量,大并发量情况下缓存失效的场景。

var select = function (callback) {
  db.select("SQL", function (results) {
    callback(results);
  });
};

大并发量情况下,一条SQL语句将被发到数据库中反复查询。

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";
    });
  }
};

使用once(侦听器完成后解耦关联)和事件队列使并发有序执行。

多异步之间的协作方案

一般一个事件对应多个侦听器,但也有一个侦听器对应多个事件的场景,比如多个异步协作:
其实面对的就是异步嵌套的难点:

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, resources) {
  done("resources", resources); 
});

通过第三方函数和第三方变量处理异步协作,这个变量count称为哨兵变量
可以使用偏函数改造:

var after = function (times, callback) {
  var count = 0, results = {};
  return function (key, value) {
    results[key] = value;
    count++;
    if (count === times) {
      callback(results);
    }
  };
};
var done = after(times, render);

这样就实现了 异步:回调=多:1的效果。

var emitter = new events.Emitter();
var done = after(times, 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);
});

实现多对多可以加入事件订阅发布

2.Promise/Deferred模式

  • 普通的事件触发方式(发布订阅模式的运行机制决定需要预先设定分支)
$.get('/api', {
  success: onSuccess,
  error: onError,
  complete: onComplete
});

在AJAX异步请求发出之前,必须先将不同事件的侦听器设置好。

  • Promise/Deferred模式
$.get('/api')
  .success(onSuccess)
  .error(onError)
  .complete(onComplete);

即使分支尚未定义,异步请求已经发出。

$.get('/api')
  .success(onSuccess1)
  .success(onSuccess2);

通过Deferred对象,不同于以前一个事件只能有一个回调,可以一个事件分别使用不同业务处理逻辑。

Promise/A

Promise/A规范包含了PromiseDeferred两部分。

Promise

Promise/A规范关于单个异步操作定义如下:

  • Promise操作只有三个状态: 未完成态 完成态 失败
  • 状态转化只能由未完成态完成态/失败转化,完成态失败不能互相转化
  • 状态转化一旦完成,不可再被修改

一个Promise对象很简单,只需要提供then()方法即可,then()有些小要求:

  • 接收两个函数作为参数
  • 一个作为状态转化为成功态的回调,一个作为状态转化为失败态的回调
  • 返回Promise对象实现链式调用
var Promise = function () {
  EventEmitter.call(this);
};
util.inherits(Promise, EventEmitter);
 
Promise.prototype.then = function (fulfilledHandler, errorHandler) {
  if (typeof fulfilledHandler === 'function') {
    // 利用once()方法,保证成功回调只执行一次
    this.once('success', fulfilledHandler);
  }
  if (typeof errorHandler === 'function') {
    // 利用once()方法,保证异常回调只执行一次
    this.once('error', errorHandler);
  }
  return this;
};

then方法中订阅了不同的事件(保存了函数参数),还需要一个状态变化来触发这些事件,这就是Deferred对象。

Deferred
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);
};

通过原型链我们关联了DeferredPromise,并且在Deferred对象中实现态变的同时,去触发Promise的事件,执行回调。

promisify
var promisify = function (res) {
  var deferred = new Deferred();
  var result = '';
  res.on('end', function () {
    deferred.resolve(result);
  });
  res.on('error', function (err) {
    deferred.reject(err);
  });
  return deferred.promise;
};

这里专门为网络请求定制了一个promisify

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

这里使用promisify包装了请求结果。

promisedeferred的差别:

  • promise主要作用于外部,通过then()方法暴露给外部自定义逻辑
  • deferred主要作用于内部,维护异步模型状态

Promise中的多异步协作

Deferred.prototype.all = function (promises) {
  var count = promises.length;
  var that = this;
  var results = [];
  promises.forEach(function (promise, i) {
    promise.then(function (data) {
      count--;
      results[i] = data;
      if (count === 0) {
        that.resolve(results);
      }
    }, function (err) {
      that.reject(err);
    });
  });
  return this.promise;
};

Promise进阶

解决异步的顺序调用问题,Promise的秘诀是对队列的操作链式调用)。

回调函数形式的解决方案
obj.api1(function (value1) {
  obj.api2(value1, function (value2) {
    obj.api3(value2, function (value3) {
      obj.api4(value3, function (value4) {
        callback(value4);
      });
    });
  });
});

回调地狱

普通函数回调形式的解决方案
var handler1 = function (value1) {
  obj.api2(value1, handler2);
};
var handler2 = function (value2) {
  obj.api3(value2, handler3);
};
var handler3 = function (value3) {
  obj.api4(value3, hander4);
};
var handler4 = function (value4) {
  callback(value4);
});
 
obj.api1(handler1);

额。。比较难懂

事件订阅(事件侦听)和发布形式的解决方案
var emitter = new event.Emitter();
 
emitter.on("step1", function () {
  obj.api1(function (value1) {
    emitter.emit("step2", value1);
  });
});
 
emitter.on("step2", function (value1) {
  obj.api2(value1, function (value2) {
    emitter.emit("step3", value2);
  });
});
 
emitter.on("step3", function (value2) {
  obj.api3(value2, function (value3) {
    emitter.emit("step4", value3);
  });
});
 
emitter.on("step4", function (value3) {
  obj.api4(value3, function (value4) {
    callback(value4);
  });
});
 
emitter.emit("step1");

是不是变得更糟了

Promise解决方案
promise()
  .then(obj.api1)
  .then(obj.api2)
  .then(obj.api3)
  .then(obj.api4)
  .then(function (value4) {
    // Do something with value4
  }, function (error) {
    // Handle any error from step1 through step4
  }).done();
Promise链式调用的实现原理
var Promise = function () {
  // 队列用于存储待执行的回调函数
  this.queue = [];
  this.isPromise = true;
};
 
Promise.prototype.then = function (fulfilledHandler, errorHandler, progressHandler) {
  var handler = {};
  if (typeof fulfilledHandler === 'function') {
    handler.fulfilled = fulfilledHandler;
  }
  if (typeof errorHandler === 'function') {
    handler.error = errorHandler;
  }
  this.queue.push(handler);
  return this;
};

上面是Promise的实现,主要用来收集回调。

var Deferred = function () {
  this.promise = new Promise();
};
 
// 完成态
Deferred.prototype.resolve = function (obj) {
  var promise = this.promise;
  var handler;
  while ((handler = promise.queue.shift())) {
    if (handler && handler.fulfilled) {
      var ret = handler.fulfilled(obj);
      if (ret && ret.isPromise) {
      // 注意这里有promise.queue队列的继承。如果then回调也是一个promise,则Defferd的promise要切换,队列要继承。
        ret.queue = promise.queue;
        this.promise = ret;
        return;
      }
    }
  }
};
 
// 失败态
Deferred.prototype.reject = function (err) {
  var promise = this.promise;
  var handler;
  while ((handler = promise.queue.shift())) {
    if (handler && handler.error) {
      var ret = handler.error(err);
      if (ret && ret.isPromise) {
        ret.queue = promise.queue;
        this.promise = ret;
        return;
      }
    }
  }
};
 
// 生成回调函数
Deferred.prototype.callback = function () {
  var that = this;
  return function (err, file) {
    if (err) {
      return that.reject(err);
    }
    that.resolve(file);
  };
};

上述是Deferred实现,主要实现内部状态的切换

var readFile1 = function (file, encoding) {
  var deferred = new Deferred();
  fs.readFile(file, encoding, deferred.callback());
  return deferred.promise;
};
var readFile2 = function (file, encoding) {
  var deferred = new Deferred();
  fs.readFile(file, encoding, deferred.callback());
  return deferred.promise;
};
 
readFile1('file1.txt', 'utf8').then(function (file1) {
  return readFile2(file1.trim(), 'utf8');
}).then(function (file2) {
  console.log(file2);
});

使用过程如上。代码有包装的操作,可以用偏函数将APIpromise化

// smooth(fs.readFile);
var smooth = function (method) {
  return function () {
    var deferred = new Deferred();
    var args = Array.prototype.slice.call(arguments, 0);
    args.push(deferred.callback());
    method.apply(null, args);
    return deferred.promise;
  };
};

使用可以简化成:

var readFile = smooth(fs.readFile);
readFile('file1.txt', 'utf8').then(function (file1) {
  return readFile(file1.trim(), 'utf8');
}).then(function (file2) {
  // file2 => I am file2
  console.log(file2);
});

3.流程控制库

尾触发与next

手动调用才能持续进行,称为尾触发,关键词是next,应用最多的是Connect的中间件

async

异步的并行执行
异步调用的依赖处理
异步的并行执行
自动依赖处理

Step

wind

四.异步并发控制

在Node中,可以方便的异步发起并发调用,未防止并发量过大,需要过载保护

异步并发控制

规则

  • 通过一个队列控制并发量
  • 活跃的异步调用(异步调用了,但是回调还没有执行)小于限定值时,从队列获取异步执行
  • 活跃调用达到限定值时,剩下的异步调用暂存队列
  • 每个异步调用结束,从队列中获取新的异步调用执行
'use strict';

const EventEmitter = require('events');

function hasOwnProperty(obj, key) {
  return Object.prototype.hasOwnProperty.call(obj, key);
}

class Bagpipe extends EventEmitter {
  constructor(limit, options = {}) {
    super();

    this.limit = limit;
    this.active = 0;
    this.queue = [];
    // queue length
    this.queueLength = Math.round(this.limit * (this.options.ratio || 1));
  }

  push(method, ...args) {
    this.queue.push({
      method: method,
      args: args
    });

    this.next();
    return this;
  }

  next() {
    // 没到限制,或者没有排队
    if (this.active >= this.limit || !this.queue.length) {
      return;
    }

    const {method, args} = this.queue.shift();

    this.active++;

    const callback = args[args.length - 1];
    var called = false;

    args[args.length - 1] = (err, ...rest) => {

      // if timeout, don't execute
      if (!called) {
        this._next();
        callback(err, ...rest);
      }
    };

    method(...args);
  }

  _next() {
    this.active--;
    this.next();
  }
}

module.exports = Bagpipe;

取自bagpipe类库源码,我做了一定删减(配置项导致,比如超时控制 拒绝模式等),方便理解。

五.总结

  • 线性思维是习惯导致。打破思维壁垒,掌握异步,享受异步编程的愉快体验
  • 阅读代码,找出最精简的描述,而后才是细枝末节
  • 编写工具/平台化代码,同编写业务代码没什么不同,需求总是最重要的。