我就是不理解continuations!

34

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

我没有计算机科学学位,我的背景是VB6 -> ASP -> ASP.NET/C#。能否有人以明确简洁的方式解释一下?

9个回答

42

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

使用这种模式,你可以在任何一行“暂停”执行,稍后再继续执行。你还可以做一些创意性的事情,比如暂时跳转到执行堆栈来检索值,或者保存当前执行状态到数据库中以便之后检索。


4
我也喜欢它,但具体哪一部分是续集呢? - user2023370
@user2023370 我认为重点在于短语“每个都接受下一行/函数作为参数来执行”。因此,继续执行意味着“接下来会执行某些操作”,并且可以显式地传递。 - Ta Thanh Dinh

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()

在这两种情况下,这些必须被特别地添加到语言中,而在具有continuations的语言中,程序员可以在没有这些内容的情况下创建它们。


10
提醒一下,这个例子不够简洁也不是非常清晰。这是一个 continuations 强大应用的演示。作为 VB/ASP/C# 程序员,您可能不熟悉系统堆栈或保存状态的概念,因此本答案的目标是演示而不是解释。
Continuations 非常灵活,可以保存执行状态并稍后恢复它。以下是使用 Scheme 中的 continuations 实现协作多线程环境的小例子:
(假设全局队列上的 enqueue 和 dequeue 操作按预期工作,此处未定义)
(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。context-switch操作在线程之间切换,并且end-process终止线程。

以下是它们使用的示例:

(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)

那些在课堂上学习过分叉和线程的人通常会得到类似于这样的示例。本文的目的是演示,通过手动保存和恢复其状态 - 其续体 - 可以在单个线程中实现类似的结果。

P.S. - 我记得《On Lisp》中有类似的内容,如果你想看专业代码,可以查看该书。


8
一种理解 continuation 的方式是将其视为处理器堆栈。当您调用 "call-with-current-continuation c" 时,它会调用您的函数 "c",并将传递给 "c" 的参数是您当前的堆栈,其中包含所有自动变量(表示为另一个函数,称之为 "k")。同时,处理器开始创建一个新的堆栈。当您调用 "k" 时,在原始堆栈上执行 "return from subroutine" (RTS) 指令,跳回到原始的 "call-with-current-continuation" ("call-cc" 从现在开始) 的上下文中,并允许您的程序像以前一样继续执行。如果您向 "k" 传递了一个参数,则此参数成为 "call-cc" 的返回值。
从您原始堆栈的角度来看,"call-cc" 看起来像是普通的函数调用。从 "c" 的角度来看,您的原始堆栈看起来像是永远不会返回的函数。
有一个关于数学家用笼子抓住狮子的老笑话:他爬进笼子里,锁上门,然后宣布自己在笼子外面,而其他所有东西(包括狮子)都在里面。Continuation 有点像笼子,而 "c" 有点像数学家。您的主程序认为 "c" 在其中,而 "c" 认为您的主程序在 "k" 中。
您可以使用 continuation 创建任意的控制流结构。例如,您可以创建一个线程库。"yield" 使用 "call-cc" 将当前 continuation 放入队列中,然后跳转到队列头部的 continuation。信号量也有其自己的挂起 continuation 队列,并且通过将其从信号量队列中取出并将其放入主队列中来重新安排线程。

4
基本上,续集是函数在停止执行后能够在稍后的时间点恢复执行的能力。在C#中,您可以使用yield关键字来实现这一点。如果您愿意,我可以提供更详细的解释,但您想要简明扼要的解释。;-)

2
在C#中,你可以访问两个续体。一个是通过 return 访问的,它可以让方法从调用它的地方继续执行。另一个是通过 throw 访问的,它可以让方法在最近的匹配的 catch 处继续执行。
有些语言允许你将这些语句视为一等值,并将它们分配并传递到变量中。这意味着你可以存储 returnthrow 的值,并在稍后真正准备好返回或抛出时调用它们。
Continuation callback = return;
callMeLater(callback);

这在许多情况下都非常方便。一个例子就像上面那样,当发生某些事情时(比如收到Web请求或其他情况),您想要暂停正在进行的工作并在稍后恢复它。
我正在几个项目中使用它们。其中一个项目中,我使用它们可以在等待网络IO时暂停程序,然后稍后恢复它。在另一个项目中,我正在编写一种编程语言,在该语言中,用户可以访问连续值,以便他们可以自己编写return和throw,或者任何其他控制流程,例如while循环,而无需我为他们做这些事情。

2
我仍在努力理解continuations,但我发现将其视为程序计数器(PC)概念的抽象之一是有用的。一个PC“指向”内存中要执行的下一条指令,但当然,该指令(以及几乎每个指令)都隐式或显式地指向其后续指令,以及应服务中断的任何指令。(即使NOOP指令也会隐式跳转到内存中的下一条指令。但如果发生中断,通常会涉及跳转到内存中的其他指令。)
程序中可能“活动”的每个点都是某种意义上的活动continuation。可以到达的其他点是潜在的active continuation,但更重要的是,它们是潜在的“计算出来”的continuation(也许是动态的),因为到达当前活动continuation之一或多个时可能会计算出它们。
这似乎与传统continuations介绍不太相符,在其中所有待处理的执行线程都明确表示为静态代码的continuation。但它考虑了这样一个事实:在通用计算机上,PC指向一个指令序列,该指令序列可能会动态地更改表示该指令序列的部分内存的内容,从而基本上在创建/修改时创建一个新的(或修改的)continuation,该continuation在之前的continuations激活时并不存在。
因此,continuation可以被视为PC的高级模型,这就是为什么它在概念上包含了普通过程调用/返回(就像古老的铁器通过低级JUMP(又名GOTO)指令加上调用时的PC记录和返回时的PC恢复来实现过程调用/返回)以及异常、线程、协程等。
因此,正如PC指向“未来”中要发生的计算一样,continuation也是同样的事情,但在更高、更抽象的层面上。PC隐式地引用内存以及绑定到任何值的所有内存位置和寄存器,而continuation则通过适当的语言抽象表示未来。
当然,虽然每台计算机(核心处理器)可能通常只有一个PC,但实际上存在许多“活动”的PC-ish实体,如上所述。中断向量包含一堆,堆栈还有更多,某些寄存器可能包含一些等。当它们的值加载到硬件PC中时,它们被“激活”,但continuation是概念的抽象,不是PC或其精确等效物(没有“主”continuation的固有概念,尽管我们经常以这些术语思考和编码,以保持事物相当简单)。
实质上,continuation是“调用时下一步该做什么”的表示,因此它可以(并且在某些语言和在continuation-passing-style程序中经常是)一种一级对象,可以像大多数其他数据类型一样实例化、传递和丢弃,就像经典计算机处理内存位置与PC的方式-几乎可以与普通整数交换。

1

想一下线程。一个线程可以运行,你可以得到它的计算结果。一个 continuation 是一个线程,你可以复制它,这样你就可以运行相同的计算两次。


1

在网络编程中,Continuations(续体)因为能很好地映射 Web 请求的暂停/恢复特性而再次受到关注。服务器可以构建一个代表用户会话的 Continuation,并在用户继续会话时恢复。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接