实现安全关闭是否需要垃圾收集?


14

我最近参加了有关编程语言的在线课程,其中介绍了闭包。我写下了两个受本课程启发的示例,以便在提出问题之前提供一些背景信息。

第一个示例是一个SML函数,该函数生成从1到x的数字列表,其中x是该函数的参数:

fun countup_from1 (x: int) =
    let
        fun count (from: int) =
            if from = x
            then from :: []
            else from :: count (from + 1)
    in
        count 1
    end

在SML REPL中:

val countup_from1 = fn : int -> int list
- countup_from1 5;
val it = [1,2,3,4,5] : int list

countup_from1函数使用帮助程序闭包count,该闭包x从其上下文捕获并使用变量。

在第二个示例中,当我调用一个函数时create_multiplier t,我得到了一个将其参数乘以t的函数(实际上是一个闭包):

fun create_multiplier t = fn x => x * t

在SML REPL中:

- fun create_multiplier t = fn x => x * t;
val create_multiplier = fn : int -> int -> int
- val m = create_multiplier 10;
val m = fn : int -> int
- m 4;
val it = 40 : int
- m 2;
val it = 20 : int

因此,变量m绑定到函数调用返回的闭包上,现在我可以随意使用它了。

现在,为了使闭包在其整个生命周期内正常工作,我们需要延长捕获变量的生命周期t(在示例中,它是整数,但可以是任何类型的值)。据我所知,在SML中,这可以通过垃圾回收来实现:闭包保留对捕获值的引用,该值随后在销毁闭包时由垃圾回收器处理。

我的问题:总的来说,垃圾回收是确保关闭安全(在整个生命周期中都可以调用)的唯一可能机制吗?

还是有什么其他机制可以确保不进行垃圾收集而关闭的有效性:复制捕获的值并将其存储在关闭中?限制闭包本身的生存期,以便在其捕获的变量到期后不能调用它?

最受欢迎的方法是什么?

编辑

我不认为可以通过将捕获的变量复制到闭包中来解释/实现上述示例。通常,捕获的变量可以是任何类型,例如,它们可以绑定到非常大的(不可变的)列表。因此,在实现中,复制这些值将非常低效。

为了完整起见,这是另一个使用引用(和副作用)的示例:

(* Returns a closure containing a counter that is initialized
   to 0 and is incremented by 1 each time the closure is invoked. *)
fun create_counter () =
    let
        (* Create a reference to an integer: allocate the integer
           and let the variable c point to it. *)
        val c = ref 0
    in
        fn () => (c := !c + 1; !c)
    end

(* Create a closure that contains c and increments the value
   referenced by it it each time it is called. *)
val m = create_counter ();

在SML REPL中:

val create_counter = fn : unit -> unit -> int
val m = fn : unit -> int
- m ();
val it = 1 : int
- m ();
val it = 2 : int
- m ();
val it = 3 : int

因此,变量也可以通过引用捕获,并且在创建变量的函数调用(create_counter ())完成后仍然有效。


2
应该保护所有未封闭的变量,使其免受垃圾回收,而未封闭的任何变量应符合垃圾收集的条件。随之而来的是,任何能够可靠地跟踪变量是否被关闭的机制都可以可靠地回收变量所占用的内存。
罗伯特·哈维

3
@btilly:Refcounting只是垃圾收集器的许多不同实现策略之一。出于这个问题的目的,如何实施GC并不重要。
约尔格W¯¯米塔格

3
@btilly:“真正的”垃圾回收是什么意思?重新计票只是实现GC的另一种方式。跟踪更为流行,可能是因为难以通过refcounting收集周期。(通常,无论如何,您最终都会得到一个单独的跟踪GC,因此,如果可以通过一个GC来解决,那么为什么还要麻烦实现两个GC。)但是还有其他处理循环的方法。1)只是禁止他们。2)忽略它们。(如果您要实现快速一次性脚本的实现,为什么不呢?)3)尝试显式检测它们。(事实证明,提供参考数量可以加快速度。)
JörgW Mittag 2013年

1
这取决于您为什么首先要关闭。例如,如果要实现完整的lambda演算语义,则肯定需要 GC句号。没有其他办法了。如果您想要某种与闭包极为相似但又不遵循闭包的确切语义的东西(例如C ++,Delphi等),则可以执行所需的任何操作,使用区域分析,使用完全手动的内存管理。
SK-logic

2
@Mason Wheeler:闭包只是值,通常无法预测它们在运行时将如何移动。从这个意义上说,它们没什么特别的,对于字符串,列表等来说同样有效。
Giorgio

Answers:


14

Rust编程语言在这方面很有趣。

Rust是一种系统语言,带有一个可选的GC,从一开始就以闭包进行设计。

作为其他变量,防锈剂有多种口味。堆栈关闭(最常见的关闭)是一次性使用的。他们生活在栈上,可以引用任何东西。拥有的闭包拥有所捕获变量的所有权。我认为它们生活在所谓的“交换堆”上,这是一个全局堆。他们的寿命取决于谁拥有他们。托管闭包存在于任务本地堆上,并由任务的GC进行跟踪。不过,我不确定它们的捕获限制。


1
非常有趣的链接和对Rust语言的引用。谢谢。+1。
Giorgio 2013年

1
在接受答案之前,我做了很多思考,因为我发现梅森的答案也非常有用。我之所以选择这种语言,是因为它既能提供信息,又引用了一种鲜为人知的语言,并采用了一种原始的闭包方法。
乔治

感谢那。我对这种年轻的语言非常热情,并且很高兴分享我的兴趣。在听说Rust之前,我不知道没有GC能否实现安全的关闭。
barjak 2013年

9

不幸的是,从GC开始会使您成为XY综合征的受害者:

  • 闭包需要的时间要长于闭包生存的时间(出于安全原因)
  • 使用GC,我们可以将这些变量的寿命延长足够长的时间
  • XY综合征:还有其他延长寿命的机制吗?

但是请注意,对于闭包而言,不需要延长变量的生存期。它只是由GC带来的;原来的安全声明只是被关闭的变量应与关闭时间一样长(即使那是不稳定的,我们可以说它们应一直存在到最后一次调用关闭之后)。

从本质上讲,我可以看到两种方法(并且有可能将它们组合在一起):

  1. 延长封闭变量的生命周期(例如,像GC一样)
  2. 限制封盖的使用寿命

后者只是一种对称方法。它不经常使用,但是如果像Rust一样,您拥有一个可识别区域的类型系统,那么肯定有可能。


7

当通过值捕获变量时,不需要垃圾收集来安全关闭。一个突出的例子是C ++。C ++没有标准的垃圾回收。C ++ 11中的Lambda是闭包(它们从周围的范围捕获局部变量)。可以将lambda捕获的每个变量指定为通过值或通过引用捕获。如果是通过引用捕获的,则可以说它是不安全的。但是,如果按值捕获变量,那么它是安全的,因为捕获的副本和原始变量是分开的,并且具有独立的生存期。

在您提供的SML示例中,很容易解释:变量是按值捕获的。无需“延长任何变量的寿命”,因为您可以将其值复制到闭包中。这是可能的,因为在ML中无法将变量分配给它。因此,一个副本和许多独立副本之间没有区别。尽管SML具有垃圾回收功能,但它与闭包捕获变量无关。

通过引用(种类)捕获变量时,也不需要垃圾收集来安全关闭。一个示例是对C,C ++,Objective-C和Objective-C ++语言的Apple Blocks扩展。在C和C ++中没有标准的垃圾回收。默认情况下,块按值捕获变量。但是,如果使用声明了局部变量__block,则块看似“通过引用”捕获了它们,并且它们是安全的-即使在定义块的范围之后也可以使用它们。这里发生的是,__block变量实际上是一个下方的特殊结构,并且在复制块时(必须复制块才能首先在范围之外使用它们),它们会“移动”用于__block 我相信通过引用计数,变量可以进入堆,并且块可以管理其内存。


4
“闭包不需要垃圾收集。”:问题是是否需要这样,以便语言可以强制执行安全的闭包。我知道我可以用C ++编写安全的闭包,但是该语言没有强制执行它们。对于可以延长捕获变量寿命的闭包,请参见对我的问题的编辑。
Giorgio

1
我想这个问题可以改写为:安全关闭
Matthieu M.

1
标题中包含“安全关闭”一词,您认为我可以用更好的方式来表述吗?
Giorgio

1
您能更正第二段吗?在SML中,闭包确实会延长捕获的变量引用的数据的生存期。另外,确实不能分配变量(更改其绑定),但确实具有可变数据(通过ref)。因此,可以,我们可以辩论闭包的实现是否与垃圾回收有关,但是应该更正上述声明。
乔治

1
@乔治:现在怎么样?另外,从什么意义上讲,您发现我的说法是闭包不需要延长捕获变量的生存期是错误的?当您谈论可变数据时,您在谈论的ref是指向结构的引用类型(s,数组等)。但是,值是引用本身,而不是引用的内容。如果您拥有var a = ref 1并制作了副本var b = a,然后使用b,是否表示您仍在使用a?您可以访问a?指向的相同结构。这就是这些类型在SML中的工作方式,并且与闭包无关
user102008 2013年

6

垃圾收集对于实现关闭不是必需的。在2008年,未进行垃圾收集的Delphi语言增加了闭包的实现。它是这样的:

编译器在幕后创建一个函子对象,该对象实现了一个表示闭包的接口。所有封闭的局部变量都从封闭过程的局部变量更改为函子对象上的字段。这样可以确保状态保持与函子一样长的时间。

此系统的局限性是函子无法捕获通过引用传递给封闭函数的任何参数以及函数的结果值,因为它们不是局部对象,其范围仅限于封闭函数的范围。

函子由闭包引用引用,它使用语法糖使它看起来像函数指针(而不是接口)一样指向开发人员。它为接口使用Delphi的引用计数系统,以确保函子对象(及其所持有的所有状态)只要需要就保持“活动”状态,然后在引用计数降至0时释放。


1
嗯,所以只能捕获局部变量,而不能捕获参数!这似乎是一个合理而明智的权衡!+1
Giorgio

1
@Giorgio:它可以捕获参数,但不能捕获属于var参数的参数。
梅森惠勒

2
您还将失去使2个闭包通过共享私有状态进行通信的能力。在基本用例中您不会遇到这种情况,但是它限制了您做复杂事情的能力。仍然是可能的绝佳范例!
btilly

3
@btilly:实际上,如果在同一个封闭函数中放置2个闭包,那是完全合法的。它们最终共享相同的仿函数对象,并且如果彼此修改相同的状态,则其中一个的更改将反映在另一个中。
梅森惠勒

2
@MasonWheeler:“不。垃圾回收本质上是不确定的;无法保证将收集到任何给定的对象,更不用说何时发生了。但是引用计数是确定性的:编译器向您保证该对象计数降至0后将立即释放。”。如果我每次都得到一角钱,我就会听到这个神话永存。OCaml具有确定性GC。C ++线程安全性shared_ptr是不确定的,因为析构函数竞相递减为零。
乔恩·哈罗普
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.