33

它们是什么,它们有什么用?

我没有 CS 学位,我的背景是 VB6 -> ASP -> ASP.NET/C#。任何人都可以用清晰简洁的方式解释它吗?

4

9 回答 9

42

想象一下,如果程序中的每一行都是一个单独的函数。每个都接受下一个要执行的行/函数作为参数。

使用此模型,您可以在任何行“暂停”执行并稍后继续执行。您还可以做一些创造性的事情,例如临时跳上执行堆栈以检索一个值,或者将当前执行状态保存到数据库中以便以后检索。

于 2008-09-02T20:57:53.410 回答
11

你可能比你想象的更了解他们。

例外是“仅向上”延续的示例。它们允许堆栈深处的代码调用异常处理程序来指示问题。

Python 示例:

try:
    broken_function()
except SomeException:
    # jump to here
    pass

def broken_function():
    raise SomeException() # go back up the stack
    # stuff that won't be evaluated

生成器是“仅向下”延续的示例。它们允许代码重新进入循环,例如,创建新值。

Python 示例:

def sequence_generator(i=1):
    while True:
        yield i  # "return" this value, and come back here for the next
        i = i + 1

g = sequence_generator()
while True:
    print g.next()

在这两种情况下,这些都必须专门添加到语言中,而在具有延续的语言中,程序员可以在它们不可用的地方创建这些东西。

于 2008-12-23T20:09:34.730 回答
9

注意,这个例子既不简洁也不特别清楚。这是一个强大的延续应用的演示。作为 VB/ASP/C# 程序员,您可能不熟悉系统堆栈或保存状态的概念,因此此答案的目的是演示而不是解释。

延续非常通用,是一种保存执行状态并在以后恢复它的方法。这是在 Scheme 中使用延续的协作多线程环境的一个小示例:

(假设操作入队和出队在此处未定义的全局队列上按预期工作)

(define (fork)
  (display "forking\n")
  (call-with-current-continuation
   (lambda (cc)
     (enqueue (lambda ()
                (cc #f)))
     (cc #t))))

(define (context-switch)
  (display "context switching\n")
  (call-with-current-continuation
   (lambda (cc)
     (enqueue
      (lambda ()
        (cc 'nothing)))
     ((dequeue)))))

(define (end-process)
  (display "ending process\n")
  (let ((proc (dequeue)))
    (if (eq? proc 'queue-empty)
        (display "all processes terminated\n")
        (proc))))

这提供了函数可以使用的三个动词——fork、context-switch 和 end-process。fork 操作分叉线程并在一个实例中返回#t,在另一个实例中返回#f。上下文切换操作在线程之间切换,并且结束进程终止一个线程。

以下是它们的使用示例:

(define (test-cs)
  (display "entering test\n")
  (cond
    ((fork) (cond
              ((fork) (display "process 1\n")
                      (context-switch)
                      (display "process 1 again\n"))
              (else (display "process 2\n")
                    (end-process)
                    (display "you shouldn't see this (2)"))))
    (else (cond ((fork) (display "process 3\n")
                        (display "process 3 again\n")
                        (context-switch))
                (else (display "process 4\n")))))
  (context-switch)
  (display "ending process\n")
  (end-process)
  (display "process ended (should only see this once)\n"))

输出应该是

entering test
forking
forking
process 1
context switching
forking
process 3
process 3 again
context switching
process 2
ending process
process 1 again
context switching
process 4
context switching
context switching
ending process
ending process
ending process
ending process
ending process
ending process
all processes terminated
process ended (should only see this once)

那些在课堂上学习过分叉和线程的人经常会给出类似的例子。这篇文章的目的是展示使用延续,您可以通过手动保存和恢复其状态 - 它的延续 - 在单个线程中实现类似的结果。

PS - 我想我记得在 On Lisp 中有类似的东西,所以如果你想看专业的代码,你应该看看这本书。

于 2008-09-02T21:33:21.560 回答
8

考虑延续的一种方法是作为处理器堆栈。当您“使用当前延续 c 调用”时,它会调用您的函数“c”,传递给“c”的参数是您当前的堆栈,其中包含所有自动变量(表示为另一个函数,称为“k ”)。与此同时,处理器开始创建一个新堆栈。当您调用“k”时,它会在原始堆栈上执行“从子程序返回”(RTS)指令,将您跳回原始“call-with-current-continuation”的上下文(从现在开始为“call-cc” on) 并允许您的程序像以前一样继续。如果您将参数传递给“k”,那么这将成为“call-cc”的返回值。

从原始堆栈的角度来看,“call-cc”看起来像一个普通的函数调用。从“c”的角度来看,您的原始堆栈看起来像一个永远不会返回的函数。

有一个古老的笑话,关于一位数学家爬进笼子,把它锁在笼子里,然后宣布自己在笼子外面,而其他东西(包括狮子)都在笼子里,从而在笼子里抓住了一只狮子。延续有点像笼子,“c”有点像数学家。你的主程序认为“c”在它里面,而“c”认为你的主程序在“k”里面。

您可以使用延续创建任意控制流结构。例如,您可以创建一个线程库。“yield”使用“call-cc”将当前延续放入队列,然后跳转到队列头部的延续。信号量也有自己的挂起延续队列,并且通过将线程从信号量队列中取出并将其放入主队列来重新调度线程。

于 2008-12-28T15:52:22.950 回答
4

基本上,延续是函数停止执行,然后在稍后的时间点从中断处恢复的能力。在 C# 中,您可以使用 yield 关键字来执行此操作。如果您愿意,我可以更详细地介绍,但您想要一个简明的解释。;-)

于 2008-09-02T21:41:53.130 回答
2

我仍然“习惯”于延续,但我认为有用的一种思考方式是作为程序计数器 (PC) 概念的抽象。PC“指向”要在内存中执行的下一条指令,但是该指令(以及几乎每条指令)当然会隐式或显式地指向后面的指令,以及应该为中断提供服务的任何指令。(即使是 NOOP 指令也会隐式跳转到内存中的下一条指令。但如果发生中断,通常会涉及跳转到内存中的其他指令。)

内存中程序中每个潜在的“活动”点,控制可能在任何给定点跳转到,在某种意义上,是一个活动的延续。可以达到的其他点是潜在的活动延续,但更重要的是,它们是由于达到一个或多个当前活动的延续而潜在地“计算”(可能是动态地)的延续。

这在对延续的传统介绍中似乎有点不合适,其中所有待处理的执行线程都明确表示为静态代码的延续;但它考虑到这样一个事实,即在通用计算机上,PC 指向的指令序列可能会改变表示该指令序列一部分的内存内容,因此本质上是创建一个新的(或修改的,如果你愿意的话) ) 动态延续,在创建/修改之前的延续激活时并不真正存在。

所以 continuation 可以看作是 PC 的高级模型,这就是为什么它在概念上包含普通的过程调用/返回(就像古代的铁通过低级 JUMP 进行过程调用/返回,又名 GOTO,指令加上记录PC on call 并在返回时恢复它)以及异常、线程、协程等。

因此,正如 PC 指出计算将在“未来”发生一样,延续做同样的事情,但在更高、更抽象的层次上。PC 隐含地指的是内存加上所有内存位置和“绑定”到任何值的寄存器,而延续则通过适合语言的抽象表示未来。

当然,虽然每台计算机(核心处理器)通常可能只有一台 PC,但实际上有许多“活动”的 PC 实体,如上文所述。中断向量包含一堆,堆栈更多,某些寄存器可能包含一些等等。当它们的值加载到硬件 PC 中时,它们被“激活”,但延续是概念的抽象,而不是 PC 或它们的精确等价物(没有“主”延续的固有概念,尽管我们经常以这些术语思考和编码以使事情变得相当简单)。

本质上,延续是“被调用时下一步做什么”的表示,因此,它可以是(并且,在某些语言和延续传递风格的程序中,通常是)一个一流的对象,它是像大多数其他数据类型一样实例化、传递和丢弃,就像经典计算机如何处理PC 相比的内存位置 - 几乎可以与普通整数互换。

于 2008-12-23T19:52:46.230 回答
2

在 C# 中,您可以访问两个延续。一个,通过 访问return,让一个方法从它被调用的地方继续。另一个,通过 访问throw,让一个方法在最近的匹配处继续catch

某些语言允许您将这些语句视为一等值,因此您可以分配它们并在变量中传递它们。这意味着您可以隐藏returnor的值,throw然后在您真正准备好返回或抛出时调用它们。

Continuation callback = return;
callMeLater(callback);

这在很多情况下都很方便。一个例子就像上面的例子,你想暂停你正在做的工作,然后在发生某些事情时恢复它(比如收到一个网络请求,或其他事情)。

我在我正在进行的几个项目中使用它们。一方面,我正在使用它们,这样我就可以在等待网络上的 IO 时暂停程序,然后再恢复它。另一方面,我正在编写一种编程语言,让用户可以访问作为值的延续,这样他们就可以自己编写return——throw或任何其他控制流,如while循环——而无需我为他们编写。

于 2008-12-28T21:12:58.483 回答
1

想想线程。一个线程可以运行,你可以得到它的计算结果。延续是您可以复制的线程,因此您可以运行相同的计算两次。

于 2008-12-28T21:37:00.837 回答
1

Continuations 对 Web 编程重新产生了兴趣,因为它们很好地反映了 Web 请求的暂停/恢复特性。服务器可以构造一个表示用户会话的继续,并在用户继续会话时恢复。

于 2008-12-29T05:59:17.640 回答