PL真有意思(六):子程序和控制抽象 (3)

在像Lisp一类的面向表达式语言中,异常处理程序被附着于表达式上,而不是语句上。在发生异常时,由于处理程序的执行将代替被保护代码中尚未结束的那一部分,因此附在表达式上的处理程序还必须为表达式提供一个值

val foo = (f(a) * b) handle Overflow => max_int 异常的实现

异常的最明显实现方式是维护一个处理程序的链接表栈。当控制进入一个受保护块时,将作用于这个块的处理程序被加到表的头部。当某个异常被引发时,语言运行时系统就弹出表中最内层的处理程序并且调用它。

在一种内部并没有提供异常的语言中,有时也可以模拟异常机制。

Scheme提供了一个名为call-with-current-continuation的通用函数。这个函数带有一个参数f,该参数本身也是函数。它调用f并将一个继续c(闭包)传给它作为参数。这个闭包包含当前的程序计数器和引用环境。在未来的任何时刻,f可以通过调用c来重新建立起所保存的环境。如果以前做过嵌套调用,控制机制就会弹出它们,就像异常所做的那样。

C的大多数版本提供了一对库例程setjmp和longjmp。setjmp以一个缓冲区作为参数,它将程序当前状态以某种形式存入其中。随后我们可以将这个缓冲区传给longjmp,要求恢复所保存的状态。

协程

有了对运行时栈的布局的理解后,我们可以考虑更一般的控制抽象的实现问题,协程。与继续一样,协程也需要用闭包表示,可以通过非局部的goto跳进来,关于协程的这种特定操作被称为transfer。这两种抽象之间的主要不同点在于:继续是一个常量,一旦创建之后就不会改变了,而协程在每次运行中都会变化。

从效果上看,一组协程在一些同时存在的上下文中执行,但在每个时刻只有一个正在执行,控制将通过命名方式在它们之间转移。协程可以用于实现迭代器和线程

栈分配

由于不同协程是并发的,因此它们不能共享同一个栈,因为作为一个整体看,它们的子程序调用和返回并不是按后进先出的顺序进行的。如果每个协程都放在词法嵌套的最外层声明处,那么它们的栈就是互不相交的

最简单的解决方案是给每个协程一块固定大小的静态分配的栈空间

转移

在从一个协程转移到另一个协程时,运行系统必须修改程序计数器、栈和处理器寄存器的内容。这些修改都被封装在transfer操作中。

对于栈的修改,最常见的方式就是简单的修改栈指针寄存器,避免在transfer中使用帧指针。在transfer开始,我们将返回地址和所有其它被调用所保存的寄存器压入当前栈,然后修改sp,由新栈中弹出新的指令地址和其它寄存器内容,然后返回

事件

事件就是在程序外部发生,出现的时间不可预测,但是需要运行中的程序相应某种情况。最常见的事件就是图形用户界面系统的输入:按键、鼠标活动。

顺序处理程序

传统上,顺序程序设计语言中事件处理程序是作为自发的子程序调用实现的,一般会使用语言之外由操作系统定义和实现的机制。为了准备好通过这种机制接受事件,一个程序将调用一个setup_handler库例程,在事件发生时将希望调用的子程序作为参数传递

在硬件层上,在P的执行期间异步设备的活动将触发一个中断机制,保持在P的寄存器,切换到一个不同的栈,并跳转到OS内核中的一个预先定义的地址上。类似的,如果另一个过程Q在中断发生时正在运行,则内核将在自己最后的时间段结束时,保存P的状态。

当一个中断发生时,主程序可能处于代码的任何位置,内核将保存状态,并通过正常的调用序列调用事件处理程序,最后恢复状态。

总结

这一篇集中关注控制抽象的问题,特别是子程序有关的问题。首先我们先了解了子程序调用栈的管理问题和维护栈的调用序列。在之后讨论了有关参数的问题,各种参数传递模型等。最后考察了异常处理机制、协程和事件。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zgyzff.html