안전한 폐쇄를 구현하려면 가비지 수집이 필요합니까?


14

나는 최근에 다른 개념들 중에서도 클로저가 제시된 프로그래밍 언어에 관한 온라인 과정에 참석했다. 저는이 과정에서 영감을 얻은 두 가지 예를 작성하여 질문을하기 전에 약간의 맥락을 제시합니다.

첫 번째 예는 1에서 x까지의 숫자 목록을 생성하는 SML 함수입니다. 여기서 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
닫힌 변수는 가비지 콜렉션으로부터 보호해야하며, 닫히지 않은 변수는 가비지 콜렉션에 적합해야합니다. 변수가 닫혔는지 여부를 안정적으로 추적 할 수있는 메커니즘도 변수가 차지하는 메모리를 안정적으로 회수 할 수 있습니다.
Robert Harvey

3
@btilly : 리 카운팅은 가비지 수집기의 여러 구현 전략 중 하나 일뿐입니다. 이 질문의 목적을 위해 GC가 어떻게 구현되는지는 중요하지 않습니다.
Jörg W Mittag 1

3
@btilly : "진정한"가비지 수집은 무엇을 의미합니까? 리 카운팅은 GC를 구현하는 또 다른 방법입니다. 추적은 재 계산으로주기를 수집하기 어렵 기 때문에 더 인기가 있습니다. (일반적으로 어쨌든 별도의 추적 GC가 발생하기 때문에 두 개의 GC를 구현하는 데 방해가되는 이유는 무엇입니까?) 그러나 사이클을 처리하는 다른 방법이 있습니다. 1) 그냥 금지하십시오. 2) 그냥 무시하십시오. (일회성 빠른 스크립트 구현을 수행하는 경우 왜 그렇지 않습니까?) 3) 명시 적으로 감지 해보십시오. (환불 가능 여부를 확인하면 속도가 빨라질 수 있습니다.)
Jörg W Mittag

1
처음에 폐쇄를 원하는 이유에 달려 있습니다. 전체 람다 미적분 의미론을 구현 하려면 GC 가 필요합니다 . 다른 방법은 없습니다. 클로저와 닮은 것을 원하지만 C ++, Delphi와 같은 정확한 의미를 따르지 않는 것을 원하는 경우 원하는 것을 수행하고 영역 분석을 사용하고 완전히 수동 메모리 관리를 사용하십시오.
SK-logic

2
@Mason Wheeler : 클로저는 단지 값일 뿐이며, 일반적으로 런타임시 어떻게 이동하는지 예측할 수 없습니다. 이런 의미에서, 그것들은 특별한 것이 아니며, 문자열,리스트 등에 대해서도 동일합니다.
Giorgio

답변:


14

Rust 프로그래밍 언어는이 측면에서 흥미 롭습니다.

Rust는 선택적인 GC를 가진 시스템 언어이며 처음부터 폐쇄 로 설계되었습니다 .

다른 변수와 마찬가지로 녹 폐쇄는 다양한 맛이 있습니다. 가장 일반적인 스택 클로저 는 원샷 사용을위한 것입니다. 그들은 스택에 살고 무엇이든 참조 할 수 있습니다. 소유 한 클로저 는 캡처 된 변수의 소유권을 갖습니다 . 나는 그들이 소위 "교환 힙"에 살고 있다고 생각합니다. 이것은 글로벌 힙입니다. 그들의 수명은 누가 그들을 소유하는지에 달려 있습니다. 관리되는 클로저 는 작업 로컬 힙에 있으며 작업의 GC에 의해 추적됩니다. 그래도 캡처 제한에 대해서는 잘 모르겠습니다.


1
Rust 언어에 대한 매우 흥미로운 링크 및 참조. 감사. +1.
Giorgio

1
Mason의 답변이 매우 유익하기 때문에 답변을 수락하기 전에 많은 것을 생각했습니다. 유익한 정보이기 때문에 클로저에 대한 독창적 인 접근 방식으로 덜 알려진 언어를 인용하기 때문에이 언어를 선택했습니다.
Giorgio

고마워 저는이 어린 언어에 대해 매우 열성적이며 관심을 공유하게되어 기쁩니다. Rust에 대해 듣기 전에 GC 없이는 안전한 폐쇄가 가능하다는 것을 몰랐습니다.
barjak

9

불행히도 GC로 시작하면 XY 증후군의 희생자가됩니다.

  • 클로저는 클로저가 수행하는 한 라이브에서 닫은 변수보다 안전 요구 사항을 요구
  • GC를 사용하면 변수의 수명을 충분히 연장 할 수 있습니다
  • XY syndrom : 수명을 연장하는 다른 메커니즘이 있습니까?

그러나 변수의 수명을 연장 한다는 아이디어 는 클로저에 필요 하지 않습니다 . 그것은 단지 GC에 의해 가져 왔습니다; 원래의 안전 진술은 변수가 클로저 기간 동안 지속되어야하는 폐쇄 된 변수에 불과 합니다 (그리고 흔들리는 경우에도 마지막 클로저 호출 후까지 살아야한다고 말할 수 있음).

본질적으로 볼 수있는 두 가지 접근 방식 있으며 잠재적으로 결합 될 수 있습니다.

  1. 예를 들어 GC처럼 폐쇄 형 변수의 수명 연장
  2. 폐쇄 수명을 제한하십시오

후자는 단지 대칭 적 접근이다. 자주 사용되지는 않지만 Rust와 같이 지역 인식 유형 시스템을 사용하는 경우 가능합니다.


7

값으로 변수를 캡처 할 때 안전한 클로저를 위해 가비지 콜렉션이 필요하지 않습니다. 하나의 두드러진 예는 C ++입니다. C ++에는 표준 가비지 콜렉션이 없습니다. C ++ 11의 람다는 클로저입니다 (주변 범위에서 지역 변수를 캡처합니다). 람다로 캡처 한 각 변수는 값 또는 참조로 캡처되도록 지정할 수 있습니다. 참조로 캡처 한 경우 안전하지 않다고 말할 수 있습니다. 그러나 변수가 값으로 캡처되면 캡처 된 사본과 원래 변수가 분리되어 독립적 인 수명을 갖기 때문에 안전합니다.

제공 한 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). 따라서 클로저 구현이 가비지 수집과 관련이 있는지 여부를 토론 할 수는 있지만 위의 내용을 수정해야합니다.
Giorgio

1
@Giorgio : 지금은 어떻습니까? 또한 어떤 의미에서 클로저가 캡처 된 변수의 수명을 연장 할 필요가 없다는 내 진술을 발견합니까? 변경 가능한 데이터에 대해 이야기 할 때 ref구조를 가리키는 참조 유형 ( s, 배열 등) 에 대해 이야기하고 있습니다 . 그러나 그 가치는 그것이 참조하는 것이 아니라 참조 자체입니다. 당신이 가지고 var a = ref 1있고 당신이 사본을 만들고 var b = a사용한다면 b, 그것은 여전히 a"아니요"를 사용하고 있다는 것을 의미 합니까? a? 예가 가리키는 동일한 구조에 액세스 할 수 있습니다 . 이러한 유형의 SML에서 작동 및 폐쇄와는 아무 상관이없는 방법 즉 그냥
user102008

6

클로저를 구현하기 위해 가비지 수집이 필요하지 않습니다. 2008 년 가비지 수집되지 않은 Delphi 언어는 클로저 구현을 추가했습니다. 다음과 같이 작동합니다.

컴파일러는 후드 아래에 클로저를 나타내는 인터페이스를 구현하는 functor 객체를 만듭니다. 모든 폐쇄 된 지역 변수는 포함 프로 시저에 대한 지역 변수에서 functor 객체의 필드로 변경됩니다. 이것은 functor가있는 동안 상태가 보존되도록합니다.

이 시스템의 한계는 함수의 결과 값뿐만 아니라 함수를 참조하여 전달 된 매개 변수가 범위가 함수의 범위에 국한된 지역이 아니기 때문에 functor에 의해 캡처 될 수 없다는 것입니다.

functor는 클로저 참조에 의해 참조되며 구문 설탕을 사용하여 인터페이스 대신 함수 포인터처럼 보이게합니다. 이 인터페이스는 인터페이스에 Delphi의 참조 계산 시스템을 사용하여 functor 객체 (및 보유하는 모든 상태)가 필요한 한 "활성화"상태를 유지하고 참조 횟수가 0으로 떨어지면 해제됩니다.


1
아, 인수가 아닌 지역 변수 만 캡처하는 것이 가능합니다! 이것은 합리적이고 영리한 타협으로 보입니다! +1
Giorgio

1
@Giorgio : var 매개 변수 가 아닌 인수 만 캡처 할 수 있습니다 .
메이슨 휠러

2
또한 공유 개인 상태를 통해 통신하는 2 개의 폐쇄를 가질 수 없습니다. 기본 유스 케이스에서는 발생하지 않지만 복잡한 작업을 수행하는 기능은 제한됩니다. 가능한 것의 여전히 훌륭한 예!
btilly

3
@btilly : 사실, 동일한 폐쇄 함수 안에 두 개의 클로저를 넣으면 완벽하게 합법적입니다. 그들은 동일한 functor 객체를 공유하게되며 서로 동일한 상태를 수정하면 한 변경 사항이 다른 변경 사항에 반영됩니다.
메이슨 휠러

2
@MasonWheeler : "아니요. 가비지 콜렉션은 본질적으로 결정적이지 않습니다. 주어진 오브젝트가 언제라도 발생하지 않고 수집 될 것이라는 보장은 없습니다. 그러나 참조 카운팅은 결정적입니다. 카운트가 0으로 떨어지면 즉시 해제됩니다. " 매번 소름이 끼쳤다면 나는 신화가 영속된다고 들었습니다. OCaml에는 결정적 GC가 있습니다. shared_ptr소멸자가 0으로 감소하기 위해 경쟁하기 때문에 C ++ 스레드 안전 은 비 결정적입니다.
Jon Harrop
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.