Node.JS

Generator与Fiber比较

 ES6的generators 和node-fibers都能够用于在等待一些I/O时暂停一个协程,而不用堵塞整个主程序。这意味着你能在代码中等待I/O返回结果,而且依然拥有单线程的好处,从而获得非堵塞I/O模型的好处。

这两个主要的区别是语法是如何显式的,这其实体现了典型的安全和灵活之间的抉择。

Generator是安全但明显得烦人

在下面代码中使用generators,将一个generator函数传递入回调函数,然后暂停这个函数执行,当回调返回时继续执行。

下面是基于gen-run编写的服务器端代码:

http.createServer(function onRequest(req, res) {

  run(handleRequest(req), function onResponseBody(err, body) {

    if (err) {

      res.statusCode = 500;

      res.setHeader("Content-Type", "text/plain");

      res.end(err.stack);

      return;

    }

    res.statusCode = 200;

    res.setHeader("Content-Type", "application/json");

    res.setHeader("Content-Length", body.length);

    res.end(body);

  });

}).listen(3000);

console.log("Server running at http:

下面写个Handler直接返回结果:

module.exports = function (req) {

  return {

    method: req.method,

    url: req.url,

    date: (new Date).toString()

  };

};

下面继续使用generator编写我们的业务逻辑,我们给每个结果加上请求数量:

var requestCount = 0;

function* handleRequest(req) {

  requestCount++;

  var result = query(req);

  result.requestCount = requestCount;

  return new Buffer(JSON.stringify(result) + "\n");

}

现在你可以如你所愿多个并发请求调用,每个都会返回一个正确的请求数量 requestCount ,这是因为JS的run-to-finish语义. 即使在generator函数体内, 任意的函数调用都不会搞乱你的代码。这体现了generator的安全优点。

generator缺点是不够灵活,如果我们加入一段慢的IO操作:

module.exports = query;

function query(req, callback) {

  // If the callback isn't passed in, return a continuable.

  if (!callback) return query.bind(this, req);

 

  // 使用计时器模拟 I/O 操作

  setTimeout(function () {

    callback(null, {

      method: req.method,

      url: req.url,

      date: (new Date).toString()

    });

  }, 100);

}

现在查询功能返回的是一个continuable或者可能是一个回调函数,而我们的处理器功能是下面这个样子:

var requestCount = 0;

function* handleRequest(req) {

  requestCount++;

  var result = yield query(req);

  result.requestCount = requestCount; // Uh-Oh!

  return new Buffer(JSON.stringify(result) + "\n");

}

我们的requestCount变量是处于一个竞争的危险状态。如果第二个请求进来,同时我们还在等待第一个请求的查询返回?他们彼此碰撞。

下面我们改造它,重写query查询作为一个 generator ,制造更多的function*和yield*:

module.exports = function* (req) {

  // 使用sleep模拟一个 I/O

  yield* sleep(100);

  // 然后返回结果

  return {

    method: req.method,

    url: req.url,

    date: (new Date).toString()

  }

};

 

function* sleep(ms) {

  yield function (callback) {

    setTimeout(callback, ms);

  };

}

这样我们可以使用委派的 yield*来替代原来直接yield:

var requestCount = 0;

function* handleRequest(req) {

  requestCount++;

  var result = yield* query(req);

  result.requestCount = requestCount; // Uh-Oh!

  return new Buffer(JSON.stringify(result) + "\n");

}

generators能够让I/O不会堵塞整个处理流程,但是这可能导致对你代码有侵入式改变。非常类似回调地狱一样。

Fibers灵活不安全

Fiber服务器端代码有点不同于gen-run:

http.createServer(function onRequest(req, res) {

  Fiber(function () {

    var body;

    try {

      body = handleRequest(req);

    }

    catch (err) {

      res.statusCode = 500;

      res.setHeader("Content-Type", "text/plain");

      res.end(err.stack);

      return;

    }

    res.statusCode = 200;

    res.setHeader("Content-Type", "application/json");

    res.setHeader("Content-Length", body.length);

    res.end(body);

  }).run();

}).listen(3000);

console.log("Server running at http:

fiber不需要yield or yield*等侵入式特别代码形式。

Handler处理器代码:

var requestCount = 0;

function handleRequest(req) {

  requestCount++;

  var result = query(req);

  result.requestCount = requestCount;

  return new Buffer(JSON.stringify(result) + "\n");

}

这里不会发生竞争,因为没有人可以暂停Fiber。

现在加入改变,如果引入了慢的IO操作,看看代码是否能易于改变:

module.exports = function (req) {

  // 使用sleep模拟 I/O

  sleep(100);

  // 返回结果

  return {

    method: req.method,

    url: req.url,

    date: (new Date).toString()

  }

};

 

var Fiber = require('fibers');

function sleep(ms) {

  var fiber = Fiber.current;

  setTimeout(function() {

      fiber.run();

  }, ms);

  Fiber.yield();

}

当服务器进入高并发负载时,这时requestCounts结果将不会很精确,几个小时痛苦的调试后发现的问题是,在handleRequest中不会改变的查询功能竟然会发生了行为变化,它暂停了fiber,让其他并发请求同时竞争访问共享的requestCount 变量。

在Node.js中使用Javascript Generators

基于Fibers开发Node.js的ExpressJS Restful服务