6 线程和协程

读完这篇文章我才意识到 python 的协程到底缺了什么,这个就是 coroutine 和 semi-coroutine 的区别了。区别就是,semi-coroutine 只能返回(yield)到调用者所在位置,不能将控制权交到任意其他协程上去。具有这个特征的,都是 semi-coroutine。

Lua 实现协程的时候,充分利用了其 C 栈和 Lua 栈。进行协程调用的时候,解释器会在 C 栈上会进行一次递归调用,然后协程自身的栈在新建立的 Lua 栈里消长。当一个协程结束的时候,解释器会退出,返回调用者(即上一个解释器)。注意,这里是一个协程对应 C 栈上的一帧。与之相反,参考经典的 CPython 实现 ,每一次 python 的函数调用都会产生一层新的 C 栈,因此,python 的函数调用受 C 堆栈大小的限制。Stackless Python 之所以声名大噪,就是因为解决了这个问题,只有 tasklet 才会在 C 栈上产生新的栈帧。

作者还提到,这里实现协程的时候,比较棘手的地方是嵌套调用时如何处理外部的局部变量,因为存在协程时,很可能会出现一个函数引用的局部变量存在于另一个协程栈中。而这个问题,因为 Lua 的 upvalue 而被完美解决了。

7 Lua 的虚拟机

Lua5.0 之前的版本,使用的都是基于栈的虚拟机,直到 5.0 才转为基于寄存器的虚拟机,Lua 是第一门大规模使用寄存器虚拟机的工业级语言。

不过要注意的是,不是说基于寄存器的虚拟机就不需要使用栈,Lua5 依然有使用栈。栈上会分配栈帧用来存放寄存器,局部变量也都在寄存器中。基于寄存器的意思是,对于 Lua 函数的参数传递,不再需要繁琐的入栈和出栈操作了。作者顺带讨论了一下寄存器虚拟机的两个性能问题,包括生成的虚拟机代码大小以及指令解码的开销。和 Java 的 jvm 做了对比,结果是难分伯仲之间,寄存器机器会稍胜一筹。

Lua 的一条指令是 32 位,指令的布局可以参考下图:

指令是三地址码的格式,A 是存放结果的寄存器,B 和 C 就是操作数。由于指令长度的限制,如果想在一个指令里完成一个跳转语句,跳转的范围就会受到限制。因此,Lua 将条件语句变成一个 test 语句和紧接着的 jump 语句来解决(跟汇编很像 ^_^)。

这节里还提到寄存器窗口,不过没看懂。维基了一下,寄存器窗口是指实际寄存器数目会比可用寄存器数目要多,不同的函数调用,他们看到的同名寄存器可以是不同的寄存器。比如 func1 使用了 AX 和 BX,func2 也同样使用 AX 和 BX,不过实际上寄存器有 4 个,func1 递归调用 func2 的时候,func2 使用的 AX 和 BX 是 func1 以外的两个。