Là thu gom rác cần thiết để thực hiện đóng cửa an toàn?


14

Gần đây tôi đã tham dự một khóa học trực tuyến về các ngôn ngữ lập trình, trong đó, trong số các khái niệm khác, việc đóng cửa đã được trình bày. Tôi viết ra hai ví dụ lấy cảm hứng từ khóa học này để đưa ra một số bối cảnh trước khi đặt câu hỏi của tôi.

Ví dụ đầu tiên là hàm SML tạo ra danh sách các số từ 1 đến x, trong đó x là tham số của hàm:

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

Trong REPL SML:

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

Các countup_from1chức năng sử dụng việc đóng cửa helper countmà chụp và sử dụng biến xtừ ngữ cảnh của nó.

Trong ví dụ thứ hai, khi tôi gọi một hàm create_multiplier t, tôi lấy lại một hàm (thực ra là một bao đóng) nhân số đối số của nó bằng t:

fun create_multiplier t = fn x => x * t

Trong REPL SML:

- 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

Vì vậy, biến mbị ràng buộc với bao đóng được trả về bởi lệnh gọi hàm và bây giờ tôi có thể sử dụng nó theo ý muốn.

Bây giờ, để việc đóng hoạt động đúng trong suốt vòng đời của nó, chúng ta cần kéo dài thời gian tồn tại của biến bị bắt t(trong ví dụ này là số nguyên nhưng nó có thể là giá trị của bất kỳ loại nào). Theo như tôi biết, trong SML, điều này có thể được thực hiện bằng cách thu gom rác: việc đóng cửa giữ một tham chiếu đến giá trị bị bắt mà sau đó được người thu gom xử lý khi đóng cửa bị phá hủy.

Câu hỏi của tôi: nói chung, việc thu gom rác là cơ chế duy nhất có thể để đảm bảo rằng việc đóng cửa an toàn (có thể gọi được trong suốt cuộc đời của họ)?

Hoặc các cơ chế khác có thể đảm bảo tính hợp lệ của các bao đóng mà không cần thu gom rác: Sao chép các giá trị đã chụp và lưu trữ bên trong bao đóng? Hạn chế thời gian đóng của chính nó để nó không thể được gọi sau khi các biến bị bắt của nó đã hết hạn?

Các phương pháp phổ biến nhất là gì?

BIÊN TẬP

Tôi không nghĩ rằng ví dụ trên có thể được giải thích / thực hiện bằng cách sao chép (các) biến đã chụp vào bao đóng. Nói chung, các biến bị bắt có thể thuộc bất kỳ loại nào, ví dụ: chúng có thể được liên kết với một danh sách rất lớn (không thay đổi). Vì vậy, trong quá trình thực hiện, việc sao chép các giá trị này sẽ rất không hiệu quả.

Để hoàn thiện, đây là một ví dụ khác sử dụng tài liệu tham khảo (và tác dụng phụ):

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

Trong REPL SML:

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

Vì vậy, các biến cũng có thể được ghi lại bằng tham chiếu và vẫn còn sống sau khi lệnh gọi hàm đã tạo chúng ( create_counter ()) đã hoàn thành.


2
Bất kỳ biến nào được đóng lại phải được bảo vệ khỏi bộ sưu tập rác và bất kỳ biến nào không được đóng lại đều đủ điều kiện để thu gom rác. Theo sau, bất kỳ cơ chế nào có thể theo dõi một cách đáng tin cậy cho dù một biến được đóng hay không cũng có thể lấy lại bộ nhớ mà biến đó chiếm một cách đáng tin cậy.
Robert Harvey

3
@btilly: Việc đếm tiền chỉ là một trong nhiều chiến lược triển khai khác nhau cho người thu gom rác. Nó không thực sự quan trọng như thế nào GC được thực hiện cho mục đích của câu hỏi này.
Jörg W Mittag

3
@btilly: Bộ sưu tập rác "thật" nghĩa là gì? Việc đếm tiền chỉ là một cách khác để thực hiện GC. Truy tìm phổ biến hơn, có lẽ là do những khó khăn trong việc thu thập các chu kỳ với việc đếm tiền. (Thông thường, dù sao bạn cũng kết thúc với một truy tìm dấu vết riêng biệt, vậy tại sao lại phải thực hiện hai GC nếu bạn có thể thực hiện bằng một.) Nhưng có nhiều cách khác để xử lý các chu kỳ. 1) Chỉ cấm họ. 2) Chỉ cần bỏ qua chúng. (Nếu bạn đang thực hiện triển khai các tập lệnh một lần nhanh chóng, tại sao không?) 3) Cố gắng phát hiện rõ ràng chúng. (Hóa ra việc có sẵn số tiền có thể tăng tốc lên.)
Jörg W Mittag

1
Nó phụ thuộc vào lý do tại sao bạn muốn đóng cửa ở nơi đầu tiên. Nếu bạn muốn thực hiện, giả sử, một ngữ nghĩa tính toán lambda đầy đủ, bạn chắc chắn cần có GC, thời gian. Không có cách nào khác xung quanh. Nếu bạn muốn một cái gì đó tương tự như đóng cửa, nhưng không tuân theo ngữ nghĩa chính xác của nó (như trong C ++, Delphi, bất cứ điều gì) - hãy làm bất cứ điều gì bạn muốn, sử dụng phân tích vùng, sử dụng quản lý bộ nhớ thủ công hoàn toàn.
SK-logic

2
@Mason Wheeler: Đóng cửa chỉ là giá trị, nói chung không thể dự đoán cách chúng sẽ được di chuyển xung quanh khi chạy. Theo nghĩa này, chúng không có gì đặc biệt, điều tương tự sẽ có giá trị cho một chuỗi, một danh sách, v.v.
Giorgio

Câu trả lời:


14

Ngôn ngữ lập trình Rust rất thú vị về khía cạnh này.

Rust là một ngôn ngữ hệ thống, với một tùy chọn GC và được thiết kế với các bao đóng ngay từ đầu.

Như các biến khác, đóng cửa rỉ sét có nhiều hương vị khác nhau. Đóng cửa ngăn xếp , những cái phổ biến nhất, là để sử dụng một lần. Họ sống trên chồng và có thể tham khảo bất cứ điều gì. Đóng cửa sở hữu có quyền sở hữu các biến bị bắt. Tôi nghĩ rằng họ sống trên cái gọi là "đống trao đổi", đó là một đống toàn cầu. Tuổi thọ của họ phụ thuộc vào người sở hữu chúng. Đóng cửa được quản lý trực tiếp trên đống nhiệm vụ cục bộ và được theo dõi bởi GC của nhiệm vụ. Tôi không chắc chắn về những hạn chế nắm bắt của họ, mặc dù.


1
Liên kết rất thú vị và tham chiếu đến ngôn ngữ Rust. Cảm ơn. +1.
Giorgio

1
Tôi đã suy nghĩ rất nhiều trước khi chấp nhận một câu trả lời vì tôi thấy câu trả lời của Mason cũng rất nhiều thông tin. Tôi đã chọn cái này vì nó vừa mang tính thông tin vừa trích dẫn một ngôn ngữ ít được biết đến hơn với cách tiếp cận nguyên bản đối với việc đóng cửa.
Giorgio

Cảm ơn vì điều đó. Tôi rất nhiệt tình với ngôn ngữ trẻ này, và tôi rất vui khi được chia sẻ sở thích của mình. Tôi không biết việc đóng cửa an toàn là có thể nếu không có GC, trước khi tôi nghe về Rust.
barjak

9

Thật không may, bắt đầu với một GC làm cho bạn trở thành nạn nhân của hội chứng XY:

  • đóng cửa yêu cầu nhiều hơn các biến họ đóng qua sống miễn là đóng cửa (vì lý do an toàn)
  • sử dụng GC chúng ta có thể kéo dài thời gian tồn tại của các biến đó đủ lâu
  • XY syndrom: có cơ chế nào khác để kéo dài tuổi thọ không?

Tuy nhiên, lưu ý, hơn là ý tưởng kéo dài thời gian tồn tại của một biến là không cần thiết cho việc đóng cửa; nó chỉ được mang đến bởi GC; tuyên bố an toàn ban đầu chỉ là các biến đóng kín nên tồn tại miễn là đóng (và thậm chí nó bị rung, chúng ta có thể nói chúng nên sống cho đến sau lần gọi đóng cuối cùng).

Về cơ bản, có hai cách tiếp cận mà tôi có thể thấy (và chúng có khả năng có thể được kết hợp):

  1. Kéo dài thời gian tồn tại của các biến đóng (ví dụ như một GC)
  2. Hạn chế tuổi thọ của việc đóng cửa

Cái sau chỉ là một cách tiếp cận đối xứng. Nó không thường được sử dụng, nhưng nếu, như Rust, bạn có một hệ thống loại nhận biết theo vùng, thì chắc chắn là có thể.


7

Bộ sưu tập rác không cần thiết cho việc đóng cửa an toàn, khi nắm bắt các biến theo giá trị. Một ví dụ nổi bật là C ++. C ++ không có bộ sưu tập rác tiêu chuẩn. Lambdas trong C ++ 11 là các bao đóng (chúng nắm bắt các biến cục bộ từ phạm vi xung quanh). Mỗi biến được bắt bởi lambda có thể được chỉ định để được bắt theo giá trị hoặc bằng tham chiếu. Nếu nó được chụp bằng tham chiếu, thì bạn có thể nói rằng nó không an toàn. Tuy nhiên, nếu một biến được bắt theo giá trị, thì nó an toàn, vì bản sao bị bắt và biến ban đầu là riêng biệt và có tuổi thọ độc lập.

Trong ví dụ SML mà bạn đã đưa ra, thật đơn giản để giải thích: các biến được nắm bắt theo giá trị. Không cần phải "kéo dài tuổi thọ" của bất kỳ biến nào vì bạn chỉ có thể sao chép giá trị của nó vào bao đóng. Điều này là có thể bởi vì, trong ML, các biến không thể được gán cho. Vì vậy, không có sự khác biệt giữa một bản sao và nhiều bản sao độc lập. Mặc dù SML có bộ sưu tập rác, nhưng nó không liên quan đến việc nắm bắt các biến bằng cách đóng.

Bộ sưu tập rác cũng không cần thiết cho việc đóng cửa an toàn khi chụp các biến bằng tham chiếu (loại). Một ví dụ là phần mở rộng Apple Blocks cho các ngôn ngữ C, C ++, Objective-C và Objective-C ++. Không có bộ sưu tập rác tiêu chuẩn trong C và C ++. Khối bắt các biến theo giá trị theo mặc định. Tuy nhiên, nếu một biến cục bộ được khai báo __block, thì các khối bắt chúng dường như "bằng tham chiếu" và chúng an toàn - chúng có thể được sử dụng ngay cả sau khi phạm vi mà khối được xác định. Điều xảy ra ở đây là __blockcác biến thực sự là một cấu trúc đặc biệt bên dưới và khi các khối được sao chép (các khối phải được sao chép để sử dụng chúng bên ngoài phạm vi ở vị trí đầu tiên), chúng "di chuyển" cấu trúc cho__block biến thành heap và khối quản lý bộ nhớ của nó, tôi tin thông qua việc đếm tham chiếu.


4
"Bộ sưu tập rác không cần thiết cho việc đóng cửa.": Câu hỏi đặt ra là liệu có cần thiết để ngôn ngữ có thể thực thi việc đóng cửa an toàn hay không. Tôi biết rằng tôi có thể viết các bao đóng an toàn trong C ++ nhưng ngôn ngữ không bắt buộc chúng. Đối với các bao đóng kéo dài thời gian tồn tại của các biến bị bắt, hãy xem chỉnh sửa cho câu hỏi của tôi.
Giorgio

1
Tôi cho rằng câu hỏi có thể được đặt lại thành: để đóng cửa an toàn .
Matthieu M.

1
Tiêu đề chứa thuật ngữ "đóng cửa an toàn", bạn có nghĩ rằng tôi có thể xây dựng nó theo cách tốt hơn không?
Giorgio

1
Bạn có thể vui lòng sửa đoạn thứ hai? Trong SML, các bao đóng sẽ kéo dài thời gian tồn tại của dữ liệu được tham chiếu bởi các biến được bắt. Ngoài ra, đúng là bạn không thể gán các biến (thay đổi ràng buộc của chúng) nhưng bạn có dữ liệu có thể thay đổi (thông qua ref). Vì vậy, OK, người ta có thể tranh luận liệu việc thực hiện đóng cửa có liên quan đến việc thu gom rác hay không, nhưng các tuyên bố trên cần được sửa chữa.
Giorgio

1
@Giorgio: Làm thế nào bây giờ? Ngoài ra, theo nghĩa nào bạn thấy tuyên bố của tôi rằng việc đóng cửa không cần kéo dài thời gian tồn tại của một biến bị bắt không chính xác? Khi bạn nói về dữ liệu có thể thay đổi, bạn đang nói về các loại tham chiếu ( refs, mảng, v.v.) chỉ đến một cấu trúc. Nhưng giá trị là chính tài liệu tham khảo, không phải là thứ mà nó chỉ ra. Nếu bạn có var a = ref 1và bạn tạo một bản sao var b = a, và bạn sử dụng b, điều đó có nghĩa là bạn vẫn đang sử dụng phải akhông? Bạn có quyền truy cập vào cùng một cấu trúc được chỉ bởi a? Có. Đó chỉ là cách các loại này hoạt động trong SML và không liên quan gì đến việc đóng cửa
user102008

6

Thu gom rác là không cần thiết để thực hiện đóng cửa. Trong năm 2008, ngôn ngữ Delphi, không phải là rác được thu thập, đã thêm một cách thực hiện các bao đóng. Nó hoạt động như thế này:

Trình biên dịch tạo một đối tượng functor dưới mui xe thực hiện Giao diện đại diện cho một bao đóng. Tất cả các biến cục bộ đã đóng được thay đổi từ cục bộ cho thủ tục kèm theo thành các trường trên đối tượng functor. Điều này đảm bảo rằng trạng thái được bảo tồn miễn là functor.

Hạn chế của hệ thống này là bất kỳ tham số nào được truyền bởi tham chiếu đến hàm kèm theo, cũng như giá trị kết quả của hàm, không thể bị bắt bởi hàm functor vì chúng không phải là địa phương có phạm vi bị giới hạn bởi hàm bao quanh.

Functor được tham chiếu bởi tham chiếu đóng, sử dụng đường cú pháp để làm cho nó trông giống nhà phát triển như một con trỏ hàm thay vì Giao diện. Nó sử dụng hệ thống đếm tham chiếu của Delphi cho các giao diện để đảm bảo rằng đối tượng functor (và tất cả trạng thái mà nó giữ) vẫn "sống" miễn là nó cần, và sau đó nó được giải phóng khi tổng số giảm xuống 0.


1
Ah, vì vậy chỉ có thể nắm bắt biến cục bộ, không phải là đối số! Đây có vẻ là một sự đánh đổi hợp lý và thông minh! +1
Giorgio

1
@Giorgio: Nó có thể nắm bắt các đối số, không phải là các tham số var .
Mason Wheeler

2
Bạn cũng mất khả năng có 2 lần đóng giao tiếp qua trạng thái riêng tư chung. Bạn sẽ không gặp phải điều đó trong các trường hợp sử dụng cơ bản, nhưng nó hạn chế khả năng làm những việc phức tạp của bạn. Vẫn là ví dụ tuyệt vời về những gì có thể!
btilly

3
@btilly: Trên thực tế, nếu bạn đặt 2 lần đóng bên trong cùng một chức năng kèm theo, điều đó hoàn toàn hợp pháp. Cuối cùng, họ chia sẻ cùng một đối tượng functor và nếu họ sửa đổi trạng thái giống nhau, các thay đổi trong một sẽ được phản ánh trong trạng thái khác.
Mason Wheeler

2
@MasonWheeler: "Không. Bộ sưu tập rác không mang tính quyết định về bản chất; không có gì đảm bảo rằng bất kỳ đối tượng cụ thể nào sẽ được thu thập, hãy để một mình khi nó xảy ra. sẽ được giải phóng ngay sau khi số đếm giảm xuống còn 0. ". Nếu tôi có một thời gian cho mỗi lần tôi nghe thấy huyền thoại đó kéo dài. OCaml có một GC xác định. An toàn chủ đề C ++ shared_ptrlà không xác định vì các hàm hủy chạy đua với số không.
Jon Harrop
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.