利用编译技术异步转同步想法

2016-05-15

写事件回调的代码把自己恶心着,思考有什么更好的做法,因为这个问题不是困扰一回两回了

去网上搜一搜,也发现什么建设性的东西,都是存在已久的话题了。查了一下promise,感觉这就是一个first class的callback的东西。callback是反人类的,这一点它没有改变本质。

知乎看到一个回答:

callcc 是功能集最小的。 程序员最容易接受的是 coroutine。

Belleve确实是极少数既理解本质并且洞察到方向的人。但是又有个蛋用呀!需要的是要解决方案。最近在写rust,在一门不支持coroutine的语言中,该怎么做?

原生语言里面实现协程库的方案很久以前我就尝试过,是有一些问题的。协程的切换需要保留上下文信息,所以都要有独立的栈。为了轻量这个栈的内存就不能太大,那么就必然面临栈扩容问题。栈扩容可以segment stack或者连续栈两种方式,前者需要很恶心的汇编,后者原生语言里其实是做不了的,因为有对象会引用到栈里面的地址,而分配一块新的栈后内存地址的引用就失效了,为了补救需要更新所有引用了老的栈地址的对象。Go可以做是因为它有运行时,垃圾回收有类型信息,它认识栈里面每块空间是什么对象,所以知道栈扩容后怎么更新引用。

这个方案我基本认为走不通。


突然脑洞大开想到一个点子。想法是这样的:利用编译技术,其实很容易把控制流暴露出来。我们可以写一个很特别的"编译器",这个编译器专门做的事情就是负责把同步代码编译成异步。然后在不支持coroutine的宿主语言那边写一个库,这个库会执行编译后的代码。

前半部分是同步转异步的翻译过程,用我们自己写的语法,编译时暴露出控制流,发现执行可能会阻塞时就切换。后半部分的库其实是一个解释器,不管是采用字节码或者其它形式。达到的效果就是我们可以用同步的思维去写代码,而底层其实是基于异步的。

不管什么样实现,控制流的切换都是需要涉及到上下文信息的。那么这个方案跟原生语言里做协程库有什么不同呢?其实这是一个interpret的协程方案,这个库就是interpret。控制流已经在前面编译的那个环节暴露出来,interpret可以实现切换自己的上下文。

会遇到的一个问题是,原生->解释器->原生,这种栈交错。如果存在这种栈交错,就无法切上下文了。好在可以保证的一点不会发生,因为遇到切换时一定会先从原生退回到解释器那一层,这样我们的上下文就全部是保留在解释器层的。

如果还是不能理解,就想一下,lua为什么能实现协程。这里想做的事情无非是这种场景的一个特化,即利用解释器这一层的协程来做同步转异步。


有idea就控制不住想试试。开始我想写成这样子

((compile exp) env cont)

LiSP第6章的编译器。但是发现如果这么做,宿主语言那边env和cont都要做成寄存器,那就需要实现一个小的虚拟机了。SECD虚拟机虽然不难,但是这样子就有点杀鸡用牛刀的感觉了:明明在写了一套toy的compiler,却骗别人说我是试试一种同步转异步的某个想法。

还是解释器吧,只需要把current continuation暴露出来,这是理解控制流的本质。LiSP第3章和eopl第5章都是讲continuation passing的interpret。本来想用scheme写,结果发现VM会把事情搞复杂,但是无VM用scheme写了没法给宿主语言用。

只好在C那边去写了。parser拿了个开源库,scheme的类型定义拿了自己以前一些老代码,吭哧吭哧一个阳光大好的周末又浪费了,活该屌丝一辈子。

代码在 https://github.com/tiancaiamao/yasfs/tree/master/value-of-k

暴露控制流没有采用callcc,callcc确实是最强大的,但是并不是最方便的。这里面提供了类似lua的几个函数,spawn,resume和yield。

这明明是个解释器嘛 ╮(╯▽╰)╭

coroutine同步continuationinterpret