Một mẫu đếm tham chiếu cho các ngôn ngữ được quản lý bộ nhớ?


11

Java và .NET có các trình thu gom rác tuyệt vời quản lý bộ nhớ cho bạn và các mẫu thuận tiện để nhanh chóng giải phóng các đối tượng bên ngoài ( Closeable, IDisposable), nhưng chỉ khi chúng được sở hữu bởi một đối tượng. Trong một số hệ thống, tài nguyên có thể cần được tiêu thụ độc lập bởi hai thành phần và chỉ được giải phóng khi cả hai thành phần giải phóng tài nguyên.

Trong C ++ hiện đại, bạn sẽ giải quyết vấn đề này bằng một shared_ptr, nó sẽ giải phóng tài nguyên một cách dứt khoát khi tất cả các tài nguyên shared_ptrbị phá hủy.

Có bất kỳ mô hình đã được chứng minh bằng tài liệu nào để quản lý và giải phóng các tài nguyên đắt tiền không có chủ sở hữu duy nhất trong các hệ thống thu gom rác không xác định theo hướng đối tượng không?


1
Bạn đã thấy Đếm tham chiếu tự động của Clang , cũng được sử dụng trong Swift chưa?
jscs

1
@JoshCaswell Vâng, và điều đó sẽ giải quyết vấn đề, nhưng tôi đang làm việc trong một không gian được thu gom rác.
C. Ross

8
Đếm tham chiếu một chiến lược Thu gom rác.
Jörg W Mittag

Câu trả lời:


15

Nói chung, bạn tránh nó bằng cách có một chủ sở hữu duy nhất - ngay cả trong các ngôn ngữ không được quản lý.

Nhưng nguyên tắc là giống nhau cho các ngôn ngữ được quản lý. Thay vì ngay lập tức đóng tài nguyên đắt tiền trên một Close()bộ đếm, bạn giảm một bộ đếm (tăng dần trên Open()/ Connect()/ etc) cho đến khi bạn đạt 0 tại thời điểm đóng thực sự đóng. Nó có thể sẽ trông và hoạt động giống như mô hình Flykg.


Đây là những gì tôi đã suy nghĩ là tốt, nhưng có một mô hình tài liệu cho nó? Fly trọng chắc chắn là tương tự, nhưng đặc biệt cho bộ nhớ như thường được xác định.
C. Ross

@ C.Ross Đây dường như là một trường hợp trong đó người quyết định được khuyến khích. Bạn có thể sử dụng một lớp bao bọc xung quanh tài nguyên không được quản lý, thêm một trình hoàn thiện vào lớp đó để giải phóng tài nguyên. Bạn cũng có thể thực hiện nó IDisposable, tiếp tục đếm để giải phóng tài nguyên càng sớm càng tốt, v.v. Có lẽ điều tốt nhất, rất nhiều lần, là có cả ba, nhưng phần cuối cùng có lẽ là phần quan trọng nhất, và việc IDisposablethực hiện là ít quan trọng nhất
Panzercrisis

11
@Panzercrisis ngoại trừ việc người hoàn thiện không được đảm bảo để chạy, và đặc biệt không được đảm bảo để chạy kịp thời .
Caleth

@Caleth Tôi đã nghĩ rằng điều đếm sẽ giúp với phần nhanh chóng. Theo như họ không chạy, bạn có nghĩa là CLR có thể không tiếp cận nó trước khi chương trình kết thúc, hoặc bạn có nghĩa là họ có thể bị loại hoàn toàn?
Panzercrisis


14

Trong một ngôn ngữ được thu thập rác (trong đó GC không xác định), không thể buộc một cách đáng tin cậy việc dọn sạch tài nguyên không phải là bộ nhớ với thời gian tồn tại của một đối tượng: Không thể nói rõ khi nào một đối tượng sẽ bị xóa. Sự kết thúc của cuộc đời hoàn toàn theo ý của người thu gom rác. GC chỉ đảm bảo rằng một đối tượng sẽ sống trong khi có thể tiếp cận được. Khi một đối tượng không thể truy cập được, nó có thể được dọn sạch tại một thời điểm nào đó trong tương lai, điều này có thể liên quan đến việc chạy bộ hoàn thiện.

Khái niệm về quyền sở hữu tài nguyên của Cameron không thực sự áp dụng trong ngôn ngữ GC. Hệ thống GC sở hữu tất cả các đối tượng.

Những ngôn ngữ này cung cấp với try-with-resource + Closable (Java), sử dụng các câu lệnh + IDis Dùng (C #) hoặc với các câu lệnh + trình quản lý bối cảnh (Python) là một cách để luồng điều khiển (! = Object) giữ một tài nguyên mà được đóng lại khi luồng điều khiển rời khỏi một phạm vi. Trong tất cả các trường hợp này, điều này tương tự như tự động chèn try { ... } finally { resource.close(); }. Thời gian tồn tại của đối tượng đại diện cho tài nguyên không liên quan đến thời gian tồn tại của tài nguyên: đối tượng có thể tiếp tục sống sau khi tài nguyên bị đóng và đối tượng có thể không truy cập được trong khi tài nguyên vẫn mở.

Trong trường hợp biến cục bộ, các cách tiếp cận này tương đương với RAII, nhưng cần được sử dụng rõ ràng tại trang web cuộc gọi (không giống như các hàm hủy C ++ sẽ chạy theo mặc định). Một IDE tốt sẽ cảnh báo khi điều này bị bỏ qua.

Điều này không hoạt động đối với các đối tượng được tham chiếu từ các vị trí khác với các biến cục bộ. Ở đây, không liên quan cho dù có một hay nhiều tài liệu tham khảo. Có thể dịch tham chiếu tài nguyên thông qua tham chiếu đối tượng sang quyền sở hữu tài nguyên thông qua luồng điều khiển bằng cách tạo một luồng riêng biệt chứa tài nguyên này, nhưng các luồng cũng là các tài nguyên cần được loại bỏ thủ công.

Trong một số trường hợp, có thể ủy quyền sở hữu tài nguyên cho một chức năng gọi. Thay vì các đối tượng tạm thời tham chiếu các tài nguyên mà họ nên (nhưng không thể) dọn sạch một cách đáng tin cậy, chức năng gọi giữ một tập hợp các tài nguyên cần được dọn sạch. Điều này chỉ hoạt động cho đến thời gian tồn tại của bất kỳ đối tượng nào trong số này tồn tại lâu hơn thời gian tồn tại của hàm và do đó tham chiếu tài nguyên đã bị đóng. Điều này không thể được phát hiện bởi trình biên dịch, trừ khi ngôn ngữ có theo dõi quyền sở hữu giống như Rust (trong trường hợp đó đã có giải pháp tốt hơn cho vấn đề quản lý tài nguyên này).

Đây là giải pháp khả thi duy nhất: quản lý tài nguyên thủ công, có thể bằng cách tự thực hiện tham chiếu. Đây là lỗi dễ xảy ra, nhưng không phải là không thể. Cụ thể, việc phải suy nghĩ về quyền sở hữu là không bình thường trong các ngôn ngữ GC, vì vậy mã hiện tại có thể không đủ rõ ràng về bảo đảm quyền sở hữu.


3

Rất nhiều thông tin tốt từ các câu trả lời khác.

Tuy nhiên, rõ ràng, mẫu bạn có thể đang tìm kiếm là bạn sử dụng các đối tượng thuộc sở hữu đơn lẻ nhỏ cho cấu trúc luồng điều khiển giống như RAII thông qua usingIDispose, kết hợp với một đối tượng (lớn hơn, có thể được tham chiếu) chứa một số (hoạt động hệ thống) tài nguyên.

Vì vậy, có các đối tượng chủ sở hữu nhỏ không chia sẻ nhỏ (thông qua cấu trúc luồng điều khiển nhỏ hơn IDisposeusingcấu trúc luồng điều khiển) có thể lần lượt thông báo cho đối tượng chia sẻ lớn hơn (có thể là tùy chỉnh Acquire& Releasephương thức).

(Các phương thức AcquireReleasehiển thị bên dưới sau đó cũng có sẵn bên ngoài cấu trúc sử dụng, nhưng không có sự an toàn của tryẩn trong using.)


Một ví dụ trong C #

void Test ( MyRefCountedClass myObj )
{
    using ( var usingRef = myObj.Acquire () )
    {
        var item = usingRef.Item;
        item.SomeMethod ();

        // the `using` automatically invokes Dispose() on usingRef
        //  which in turn invokes Release() on `myObj.
    }
}

interface IReferencable<T> where T: IReferencable<T> {
    Reference<T> Acquire ();
    void Release();
}

struct Reference<T>: IDisposable where T: IReferencable<T>
{
    public readonly T Item;
    public Reference(T item) { Item = item; _released = false; }
    public void Dispose() { if (! _released ) { _released = true; Item.Release(); } }
    private bool _released;
}

class MyRefCountedClass : IReferencable<MyRefCountedClass>
{
    private int _refCount = 0;

    public Reference<MyRefCountedClass> Acquire ()
    {
        _refCount++;
        return new Reference<MyRefCountedClass>(this);
    }

    public void Release ()
    {
        if (--_refCount <= 0)
            Dispose();
    }

    // NOTE that MyRefCountedClass does not have to implement IDisposable, but it can...
    // as shown here it doesn't implement the interface
    private void Dispose ()  
    {
        if ( _refCount > 0 )
            throw new Exception ("Dispose attempted on item in use.");
        // release other resources...
    }

    public int SomeMethod()
    {
        return 0;
    }
}

Nếu đó phải là C # (trông giống như vậy) thì việc triển khai <T> Tham chiếu của bạn là không chính xác. Hợp đồng cho IDisposable.Disposecác quốc gia gọi Disposenhiều lần trên cùng một đối tượng phải là không có. Nếu tôi thực hiện một mô hình như vậy, tôi cũng sẽ đặt Releaseriêng tư để tránh các lỗi không cần thiết và sử dụng ủy quyền thay vì kế thừa (loại bỏ giao diện, cung cấp một SharedDisposablelớp đơn giản có thể được sử dụng với Dispose tùy ý), nhưng đó là vấn đề quan trọng hơn.
Voo

@Voo, ok, điểm tốt, thx!
Erik Eidt

1

Phần lớn các đối tượng trong một hệ thống thường phù hợp với một trong ba mẫu:

  1. Các đối tượng có trạng thái sẽ không bao giờ thay đổi và các tham chiếu được giữ hoàn toàn như một phương tiện để đóng gói trạng thái. Các thực thể chứa các tham chiếu không biết cũng không quan tâm đến việc liệu có thực thể nào khác giữ các tham chiếu đến cùng một đối tượng hay không.

  2. Các đối tượng nằm dưới sự kiểm soát độc quyền của một thực thể duy nhất, là chủ sở hữu duy nhất của tất cả các trạng thái trong đó và sử dụng đối tượng hoàn toàn như một phương tiện để đóng gói trạng thái (có thể thay đổi) trong đó.

  3. Các đối tượng được sở hữu bởi một thực thể duy nhất, nhưng các thực thể khác được phép sử dụng theo những cách hạn chế. Chủ sở hữu của đối tượng có thể sử dụng nó không chỉ như một phương tiện đóng gói trạng thái, mà còn gói gọn một mối quan hệ với các thực thể khác có chung nó.

Theo dõi bộ sưu tập rác hoạt động tốt hơn so với đếm tham chiếu cho # 1, bởi vì mã sử dụng các đối tượng đó không cần phải làm gì đặc biệt khi nó được thực hiện với tham chiếu cuối cùng còn lại. Đếm tham chiếu không cần thiết cho # 2 vì các đối tượng sẽ có chính xác một chủ sở hữu và nó sẽ biết khi nào nó không còn cần đối tượng nữa. Kịch bản # 3 có thể gây ra một số khó khăn nếu chủ sở hữu của một đối tượng giết chết nó trong khi các thực thể khác vẫn giữ các tham chiếu; ngay cả ở đó, một GC theo dõi có thể tốt hơn so với đếm tham chiếu để đảm bảo rằng các tham chiếu đến các đối tượng chết vẫn có thể được xác định một cách đáng tin cậy như các tham chiếu đến các đối tượng chết, miễn là có bất kỳ tham chiếu nào như vậy tồn tại.

Có một vài tình huống có thể cần phải có một đối tượng không có chủ sở hữu có thể chia sẻ và nắm giữ các tài nguyên bên ngoài miễn là bất cứ ai cần dịch vụ của nó và nên giải phóng chúng khi dịch vụ của nó không còn cần thiết nữa. Ví dụ, một đối tượng đóng gói nội dung của tệp chỉ đọc có thể được chia sẻ và sử dụng đồng thời bởi nhiều thực thể mà không ai trong số họ phải biết hoặc quan tâm đến sự tồn tại của nhau. Những trường hợp như vậy là rất hiếm, tuy nhiên. Hầu hết các đối tượng sẽ có một chủ sở hữu rõ ràng hoặc không có chủ sở hữu rõ ràng. Nhiều quyền sở hữu là có thể, nhưng hiếm khi hữu ích.


0

Quyền sở hữu chung hiếm khi có ý nghĩa

Câu trả lời này có thể hơi lạc lõng, nhưng tôi phải hỏi, có bao nhiêu trường hợp có ý nghĩa từ quan điểm của người dùng để chia sẻ quyền sở hữu ? Ít nhất là trong các lĩnh vực tôi đã làm việc, thực tế không có lĩnh vực nào vì nếu không, điều đó có nghĩa là người dùng không cần đơn giản xóa một thứ gì đó khỏi một nơi, nhưng xóa nó khỏi tất cả các chủ sở hữu có liên quan trước khi tài nguyên thực sự loại bỏ khỏi hệ thống.

Đó thường là một ý tưởng kỹ thuật cấp thấp hơn để ngăn chặn tài nguyên bị phá hủy trong khi một thứ khác vẫn đang truy cập vào nó, giống như một luồng khác. Thông thường khi người dùng yêu cầu đóng / xóa / xóa thứ gì đó khỏi phần mềm, thì nên xóa nó càng sớm càng tốt (bất cứ khi nào an toàn để gỡ bỏ) và chắc chắn không nên nán lại và gây rò rỉ tài nguyên miễn là ứng dụng đang chạy

Ví dụ, một tài sản trò chơi trong trò chơi video có thể tham chiếu một tài liệu từ thư viện tài liệu. Chúng tôi chắc chắn không muốn, giả sử, một sự cố con trỏ lơ lửng nếu tài liệu bị xóa khỏi thư viện tài liệu trong một luồng trong khi một luồng khác vẫn đang truy cập vào tài liệu được tham chiếu bởi tài sản trò chơi. Nhưng điều đó không có nghĩa là nó có ý nghĩa đối với tài sản trò chơi để chia sẻ quyền sở hữu tài liệu mà họ tham chiếu với thư viện tài liệu. Chúng tôi không muốn buộc người dùng xóa tài liệu khỏi cả thư viện tài sản và tài liệu. Chúng tôi chỉ muốn đảm bảo rằng các tài liệu không bị xóa khỏi thư viện tài liệu, chủ sở hữu duy nhất của tài liệu, cho đến khi các chủ đề khác kết thúc truy cập vào tài liệu.

Rò rỉ tài nguyên

Tuy nhiên, tôi đã làm việc với một nhóm trước đây bao gồm tất cả các thành phần trong phần mềm. Và trong khi điều đó thực sự có ích trong việc đảm bảo chúng tôi không bao giờ bị phá hủy tài nguyên trong khi các luồng khác vẫn đang truy cập vào chúng, thì cuối cùng chúng tôi đã nhận được phần rò rỉ tài nguyên .

Và đây không phải là những rò rỉ tài nguyên tầm thường mà chỉ làm đảo lộn các nhà phát triển, giống như một kilobyte bộ nhớ bị rò rỉ sau một phiên kéo dài một giờ. Đây là những rò rỉ sử thi, thường là hàng gigabyte bộ nhớ trong một phiên hoạt động, dẫn đến các báo cáo lỗi. Bởi vì bây giờ khi quyền sở hữu của tài nguyên đang được tham chiếu (và do đó được chia sẻ quyền sở hữu) trong số 8 phần khác nhau của hệ thống, thì chỉ mất một phần để không xóa tài nguyên để phản hồi người dùng yêu cầu xóa tài nguyên đó bị rò rỉ và có thể vô thời hạn.

Vì vậy, tôi chưa bao giờ là một fan hâm mộ lớn của GC hoặc tính tham chiếu được áp dụng ở bất kỳ quy mô rộng nào vì họ dễ dàng tạo ra phần mềm bị rò rỉ như thế nào. Những gì trước đây là một sự cố con trỏ lơ lửng, dễ dàng phát hiện biến thành một rò rỉ tài nguyên rất khó phát hiện, có thể dễ dàng bay theo radar thử nghiệm.

Tài liệu tham khảo yếu có thể giảm thiểu vấn đề này nếu ngôn ngữ / thư viện cung cấp những vấn đề này, nhưng tôi thấy khó có được một nhóm các nhà phát triển các kỹ năng hỗn hợp để có thể sử dụng nhất quán các tài liệu tham khảo yếu bất cứ khi nào thích hợp. Và khó khăn này không chỉ liên quan đến nhóm nội bộ, mà là với mọi nhà phát triển plugin duy nhất cho phần mềm của chúng tôi. Chúng cũng có thể dễ dàng khiến hệ thống rò rỉ tài nguyên bằng cách lưu trữ một tài liệu tham khảo liên tục đến một đối tượng theo cách gây khó khăn cho việc truy tìm lại plugin là thủ phạm, vì vậy chúng tôi cũng nhận được phần báo cáo lỗi của chúng tôi từ tài nguyên phần mềm của chúng tôi bị rò rỉ đơn giản vì một plugin có mã nguồn nằm ngoài tầm kiểm soát của chúng tôi không thể phát hành tài liệu tham khảo cho các tài nguyên đắt tiền đó.

Giải pháp: Trì hoãn, Loại bỏ định kỳ

Vì vậy, giải pháp của tôi sau này mà tôi đã áp dụng cho các dự án cá nhân mang lại cho tôi loại tốt nhất tôi tìm thấy từ cả hai thế giới là loại bỏ khái niệm đó referencing=ownershipnhưng vẫn trì hoãn việc phá hủy tài nguyên.

Kết quả là, bây giờ bất cứ khi nào người dùng làm điều gì đó khiến tài nguyên cần xóa, API được thể hiện dưới dạng chỉ xóa tài nguyên:

ecs->remove(component);

... Mô hình logic kết thúc người dùng theo cách rất đơn giản. Tuy nhiên, tài nguyên (thành phần) có thể không bị xóa ngay lập tức nếu có các luồng hệ thống khác trong giai đoạn xử lý của chúng, nơi chúng có thể truy cập cùng một thành phần.

Vì vậy, các luồng xử lý này sau đó mang lại thời gian ở đây và ở đó cho phép một luồng giống như trình thu gom rác thức dậy và " ngăn chặn thế giới " và phá hủy tất cả các tài nguyên được yêu cầu xóa trong khi khóa các luồng xử lý các thành phần đó cho đến khi hoàn thành . Tôi đã điều chỉnh điều này để khối lượng công việc cần thực hiện ở đây nói chung là tối thiểu và không cắt giảm đáng kể vào tốc độ khung hình.

Bây giờ tôi không thể nói đây là một số phương pháp đã được thử nghiệm và được chứng minh bằng tài liệu tốt, nhưng đó là thứ tôi đã sử dụng trong vài năm nay mà không bị đau đầu và không bị rò rỉ tài nguyên. Tôi khuyên bạn nên tìm hiểu các cách tiếp cận như thế này khi kiến ​​trúc của bạn có thể phù hợp với loại mô hình đồng thời này vì nó ít nặng tay hơn so với GC hoặc đếm lại và không có nguy cơ rò rỉ tài nguyên này dưới radar thử nghiệm.

Một nơi mà tôi thấy việc đếm lại hoặc GC là hữu ích là cho các cấu trúc dữ liệu liên tục. Trong trường hợp đó, đó là lãnh thổ cấu trúc dữ liệu, tách biệt khỏi mối quan tâm của người dùng và thực sự có ý nghĩa đối với mỗi bản sao bất biến để có khả năng chia sẻ quyền sở hữu cùng một dữ liệu chưa được sửa đổi.

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.