Call/CC与Node.js
发布于 13 年前 作者 zealot 9265 次浏览 最后一次编辑是 8 年前

<em>本文介绍一个使用大概10行代码, 实现一个类似<a href=“http://blog.zhaojie.me/2010/11/asynciterator-the-asyncenumerator-in-javascript.html”>老赵JSCEX</a>的工具</em><br /> <br/><br /> <br/>首先来看一下下面这段Node.js代码:<br /> <br/><br> <br/><pre> <br/>var copy = function(src, dst) { <br/> var fd_src = fs.open(src, “r”); <br/> var fd_dst = fs.open(dst, “w”); <br/> while (true) { <br/> var buffer_size = 4096; <br/> var buffer = new Buffer(buffer_size); <br/> var bytes_read = fs.read(fd_src, buffer, 0, buffer_size); <br/> if (bytes_read > 0) { <br/> var write_at = 0; <br/> while (write_at < bytes_read) { <br/> write_at += fs.write(fd_dst, buffer, write_at, bytes_read - write_at); <br/> } <br/> } else { <br/> break; <br/> } <br/> } <br/>}; <br/></pre> <br/><br> <br/>这段看起来像是一个文件拷贝的函数, 但是这个函数在Node里面是无法运行的, 因为Node的所有函数调用都必须是异步的, 这里用到的fs.open, fs.read, fs.write都必须修改为异步调用, 修改后可运行的代码如下:<br /> <br/><br> <br/><pre> <br/>var copy = function(src, dst) { <br/> fs.open(src, “r”, undefined, function(err, fd_src) { <br/> fs.open(dst, “w”, undefined, function(err, fd_dst) { <br/> var copy_rec = function(position) { <br/> var buffer_size = 4096; <br/> var buffer = new Buffer(4096); <br/> fs.read(fd_src, buffer, 0, buffer_size, position, function(err, bytes_read) { <br/> if (bytes_read > 0) { <br/> var write_rec = function(write_at) { <br/> if (write_at 0) { <br/> write_rec(write_at + written); <br/> } <br/> }); <br/> } else { <br/> copy_rec(position + bytes_read); <br/> } <br/> }; <br/> write_rec(0); <br/> } <br/> }); <br/> }; <br/> copy_rec(0); <br/> }); <br/> }); <br/>}; <br/></pre> <br/><br> <br/>看到这里, 我想第一次接触Node的同学就已经要崩溃了, 一个简单的文件拷贝函数竟然需要嵌套4~5层函数回调, 那么更复杂的比如web servlet岂不是要嵌套10多层函数回调了?<br /> <br/><br /> <br/>难道没有方法让最上面的简洁明了的代码运行起来么? 下面就介绍一种方法, <strong>不修改最开始那个简洁明了的拷贝函数, 让它直接跑起来, 并且不会阻塞整个JS引擎</strong><br /> <br/><br /> <br/>首先分析一下同步调用与异步调用的区别<br /> <br/><pre> <br/>// sync <br/>var fd = fs.read (name, mode); <br/>// use fd <br/> <br/> <br/>// async <br/>fs.read (name, mode, function (fd) { <br/> // use fd <br/>}); <br/></pre> <br/><br> <br/>仔细观察这两个调用, 会发现其实这就是一个<a href=“http://en.wikipedia.org/wiki/Continuation-passing_style”>CPS变换</a>, 所以, 我们只需要做一个CPS变换的包装就可以实现同步的文件操作, 具体实现如下:<br /> <br/><br> <br/><pre> <br/>var efs = { <br/> open: function (name, mode) { <br/> return call_cc(function (continuation) { <br/> fs.open(name, mode, undefined, function(err, fd_src) { <br/> continuation(fd_src); <br/> }); <br/> }); <br/> }, <br/> read: function (fd, buffer, offset, length) { <br/> return call_cc(function (continuation) { <br/> fs.read(fd, buffer, offset, length, null, function(err, bytes_read) { <br/> continuation(bytes_read); <br/> }); <br/> }); <br/> }, <br/> write: function (fd, buffer, offset, length) { <br/> return call_cc(function (continuation) { <br/> fs.write(fd, buffer, offset, length, null, function(err, written) { <br/> continuation(written); <br/> }); <br/> }); <br/> } <br/>}; <br/></pre> <br/><br> <br/>这个实现的核心就是<a href=“http://en.wikipedia.org/wiki/Call-with-current-continuation”>call/cc函数</a>, 如果了解lisp或者scheme的同学可能会非常熟悉这个函数, 这个函数作用是把当前正在运行的线程挂起, 然后执行call/cc本体, call/cc函数<strong>不会返回</strong>, 直到主动调用参数传进来的那个回调函数(本例中continuation变量绑定的函数), call/cc函数的返回值就是传给回调函数的参数. call/cc函数确实很难理解, 下面举例说明一下:<br> <br/><br> <br/><pre> <br/>util.log(“before call/cc”); <br/>var result = call_cc(function (continuation) { <br/> util.log(“in call/cc before set timer”); <br/> setTimeout(function () { <br/> util.log(“before callback”); <br/> continuation(“hello world!”); <br/> util.log(“after callback”); <br/> }, 1000); <br/> util.log(“in call/cc after set timer”); <br/>}); <br/>util.log(“after call/cc " + result); <br/></pre> <br/><br> <br/>这段代码的输出为:<br> <br/><br> <br/><pre> <br/>31 Oct 13:01:33 - before call/cc <br/>// 运行call/cc函数主体 <br/>31 Oct 13:01:33 - in call/cc before set timer <br/>31 Oct 13:01:33 - in call/cc after set timer <br/>// 休息1秒, 进入setTimeout的回调 <br/>31 Oct 13:01:34 - before callback <br/>// 进入continuation函数, 调用continuation函数就相当于call/cc函数返回了"hello world!”, 从call/cc返回开始继续执行 <br/>31 Oct 13:01:34 - after call/cc hello world! <br/>// 执行setTimeout回调剩下部分 <br/>31 Oct 13:01:34 - after callback <br/></pre> <br/><br> <br/>现在我们已经使用CPS转换得到了"同步"的文件处理函数, 虽然说看起来是同步的, 但是内部使用的是call/cc实现, 实际上是不会阻塞JS引擎的, 其他回调仍然能正常执行. 有了这些函数的支持, 本文开头给出的那个文件拷贝函数就能够<strong>不作任何修改正常执行</strong>, 现在一切问题都已经解决了, 唯一剩下的问题就是call/cc函数的实现, 标准的node.js引擎是没有call/cc函数的, 这里我们使用<a href=“https://github.com/laverdet/node-fibers”>node-fibers</a>实现call/cc函数.<br> <br/><br> <br/><pre> <br/>var call_cc = function(cont) { <br/> var fiber = Fiber.current; <br/> cont(function (a) { <br/> fiber.run(a); <br/> }); <br/> return Fiber"yield"; <br/>}; <br/></pre> <br/><br> <br/>由于node-fiber的限制, 这个copy函数必须运行在fiber环境中, 完整代码如下:<br> <br/><br> <br/><pre> <br/>var fs = require(“fs”); <br/>require(“fibers”); <br/> <br/>var call_cc = function(cont) { <br/> var fiber = Fiber.current; <br/> cont(function (a) { <br/> fiber.run(a); <br/> }); <br/> return Fiber"yield"; <br/>}; <br/> <br/>var efs = { <br/> open: function (name, mode) { <br/> return call_cc(function (continuation) { <br/> fs.open(name, mode, undefined, function(err, fd_src) { <br/> continuation(fd_src); <br/> }); <br/> }); <br/> }, <br/> read: function (fd, buffer, offset, length) { <br/> return call_cc(function (continuation) { <br/> fs.read(fd, buffer, offset, length, null, function(err, bytes_read) { <br/> continuation(bytes_read); <br/> }); <br/> }); <br/> }, <br/> write: function (fd, buffer, offset, length) { <br/> return call_cc(function (continuation) { <br/> fs.write(fd, buffer, offset, length, null, function(err, written) { <br/> continuation(written); <br/> }); <br/> }); <br/> } <br/>}; <br/> <br/>var copy = function(src, dst) { <br/> var fd_src = efs.open(src, “r”); <br/> var fd_dst = efs.open(dst, “w”); <br/> while (true) { <br/> var buffer_size = 4096; <br/> var buffer = new Buffer(buffer_size); <br/> var bytes_read = efs.read(fd_src, buffer, 0, buffer_size); <br/> if (bytes_read > 0) { <br/> var write_at = 0; <br/> while (write_at < bytes_read) { <br/> write_at += efs.write(fd_dst, buffer, write_at, bytes_read - write_at); <br/> } <br/> } else { <br/> break; <br/> } <br/> } <br/>}; <br/> <br/>Fiber(function () { <br/> copy(“src”, “dst”); <br/>}).run(); <br/></pre> <br/><br> <br/><br> <br/>最后一点空间, 我想简单评论一下老赵的JSCEX, 简单来说, 就是一点: <strong>简直就是在重复发明轮子</strong>.<br> <br/>有关函数式编程, 在lisp界已经有40~50年的研究, 各个方面都已经有非常完善的体系, 当然这种异步同步函数之间互相转换早已经有<a href=“http://doi.acm.org/10.1145/91556.91622”>体系化, 可证明的研究</a>, 实现这样一个异步类库, 最好是从底层做起, 首先实现fiber或者<a href=“http://doi.acm.org/10.1145/1596550.1596596”>shift/reset</a>这样的底层函数, 然后再其之上, 构建一个异步框架. 像老赵这样, 一上来就搞JS再编译, 那是绕了十万八千里的远路, 不仅本身会引入bug, 也会给用户的调试造成很大困扰, 性能问题也无法保证.<br> <br/><br>

4 回复

我想老赵编写JSCEX的初衷是为了在浏览器上运行,而不是服务器端,浏览器端无法实现类似fiber的功能,所以才有了JSCEX。

JSCEX参考的原型应该是C#中的async/await, 而这个async/await实际上是被裁减的shift/reset, 在Scala中shift/reset是通过CPS实现的 <br/> <br/>对于一个类库来说, 通过再编译实现其功能, 我的感觉就是 不靠谱

@Zealot <br/> <br/>Jscex参考的原型是F#,其原理是Haskell的Monad,它的实现方式和C#完全不同。这点看看Jscex编译后的结果就行了,说到底就是CPS,别急着下结论和“应该”。LISP是几十年的研究成果,Haskell也是几十年的研究成果,不用厚此薄彼。 <br/> <br/>你这个所谓10行实现,是基于一个运行时的修改,按照你的说法本身也会引入bug,调试困难──Jscex的调试反而极其简单,因为它的编译/反编译模式用人眼就能完成。 <br/> <br/>当然,最后也可以说一句,C#其实也可以看作是CPS,有机会可以详细讨论。

还有得多说一句这个“再编译”,这一定要搞清楚的是,Jscex是“无编译”,它只是用到了编译技术里常见的手段而已,但事实上没有什么编译过程。开发人员写的是直接的JavaScript,在浏览器或是NodeJS里直接运行,运行的直接就是你写的JavaScript。这跟各种编译实现的CoffeeScript或是C#,Java到JavaScript的技术完全不是一码事情,所以别被吓倒了,直接认为不靠谱或是重量级。事实上Jscex在运行时只有不到2K(gzipped)的核心库,没多少类库比它还轻量。

回到顶部