Pola penghitungan referensi untuk bahasa yang dikelola memori?


11

Java dan .NET memiliki pengumpul sampah yang hebat yang mengelola memori untuk Anda, dan pola yang mudah digunakan untuk melepaskan objek eksternal ( Closeable, IDisposable) dengan cepat, tetapi hanya jika mereka dimiliki oleh satu objek. Dalam beberapa sistem, sumber daya mungkin perlu dikonsumsi secara mandiri oleh dua komponen, dan hanya dilepaskan ketika kedua komponen melepaskan sumber daya.

Dalam C + + modern Anda akan memecahkan masalah ini dengan shared_ptr, yang akan secara pasti melepaskan sumber daya ketika semua shared_ptrhancur.

Apakah ada pola yang terdokumentasi dan terbukti untuk mengelola dan mengeluarkan sumber daya mahal yang tidak memiliki pemilik tunggal dalam sistem pengumpulan sampah yang berorientasi objek dan non-deterministik?


1
Pernahkah Anda melihat Penghitungan Referensi Otomatis Clang , juga digunakan dalam Swift ?
jscs

1
@ JoshCaswell Ya, dan itu akan menyelesaikan masalah, tapi saya sedang bekerja di tempat sampah.
C. Ross

8
Penghitungan Referensi adalah strategi Pengumpulan Sampah.
Jörg W Mittag

Jawaban:


15

Secara umum, Anda menghindarinya dengan memiliki satu pemilik - bahkan dalam bahasa yang tidak dikelola.

Tetapi prinsipnya sama untuk bahasa yang dikelola. Alih-alih segera menutup sumber daya mahal pada Close()Anda penurunan counter (bertambah pada Open()/ Connect()/ dll) sampai Anda menekan 0 pada titik mana penutupan sebenarnya melakukan penutupan. Ini kemungkinan akan terlihat dan bertindak seperti Pola Bobot Terbang.


Ini adalah apa yang saya pikirkan juga, tetapi apakah ada pola yang terdokumentasi untuk itu? Bobot terbang tentu mirip, tetapi khusus untuk memori seperti yang biasanya ditentukan.
C. Ross

@ C.Ross Ini sepertinya merupakan kasus di mana para finalis didorong. Anda bisa menggunakan kelas pembungkus di sekitar sumber daya yang tidak dikelola, menambahkan finalizer ke kelas itu untuk melepaskan sumber daya. Anda juga dapat menerapkannya IDisposable, menghitung untuk melepaskan sumber daya sesegera mungkin, dll. Mungkin yang terbaik, sering kali, adalah memiliki ketiganya, tetapi finalizer mungkin adalah bagian yang paling penting, dan IDisposableimplementasinya adalah yang paling tidak kritis.
Panzercrisis

11
@Panzercrisis kecuali bahwa finalizers tidak dijamin untuk berjalan, dan terutama tidak dijamin untuk segera berjalan .
Caleth

@ Caleth saya berpikir hal penting akan membantu dengan bagian ketepatan waktu. Sejauh mereka tidak berjalan sama sekali, apakah maksud Anda CLR mungkin tidak menyiasati sebelum program berakhir, atau apakah Anda berarti mereka mungkin didiskualifikasi langsung?
Panzercrisis


14

Dalam bahasa sampah yang dikumpulkan (di mana GC tidak deterministik), tidak mungkin untuk secara andal mengikat pembersihan sumber daya selain memori ke masa pakai suatu objek: Tidak mungkin untuk menyatakan kapan suatu objek akan dihapus. Akhir masa hidup sepenuhnya tergantung pada kebijaksanaan pemulung. GC hanya menjamin bahwa suatu benda akan hidup saat benda itu dapat dijangkau. Setelah suatu objek menjadi tidak terjangkau, mungkin akan dibersihkan di beberapa titik di masa depan, yang mungkin melibatkan menjalankan finalizer.

Konsep "kepemilikan sumber daya" tidak benar-benar berlaku dalam bahasa GC. Sistem GC memiliki semua objek.

Apa yang ditawarkan oleh bahasa ini dengan coba-dengan-sumber daya + Closeable (Java), menggunakan pernyataan + IDisposable (C #), atau dengan pernyataan + manajer konteks (Python) adalah cara untuk mengontrol aliran (! = Objek) untuk menampung sumber daya yang ditutup ketika aliran kontrol meninggalkan ruang lingkup. Dalam semua kasus ini, ini mirip dengan yang dimasukkan secara otomatis try { ... } finally { resource.close(); }. Masa hidup objek yang mewakili sumber daya tidak terkait dengan masa pakai sumber daya: objek dapat terus hidup setelah sumber daya ditutup, dan objek mungkin menjadi tidak terjangkau saat sumber daya masih terbuka.

Dalam kasus variabel lokal, pendekatan ini setara dengan RAII, tetapi perlu digunakan secara eksplisit di situs panggilan (tidak seperti destruktor C ++ yang akan berjalan secara default). IDE yang baik akan memperingatkan ketika ini dihilangkan.

Ini tidak berfungsi untuk objek yang dirujuk dari lokasi selain variabel lokal. Di sini, tidak relevan apakah ada satu atau lebih referensi. Dimungkinkan untuk menerjemahkan referensi sumber daya melalui referensi objek ke kepemilikan sumber daya melalui aliran kontrol dengan membuat utas terpisah yang menampung sumber daya ini, tetapi utas juga adalah sumber daya yang harus dibuang secara manual.

Dalam beberapa kasus dimungkinkan untuk mendelegasikan kepemilikan sumber daya ke fungsi panggilan. Alih-alih objek sementara referensi sumber daya bahwa mereka harus (tetapi tidak dapat) membersihkan dengan andal, fungsi panggilan memegang satu set sumber daya yang perlu dibersihkan. Ini hanya berfungsi sampai masa hidup dari salah satu objek ini hidup lebih lama dari fungsi, dan karena itu referensi sumber daya yang telah ditutup. Ini tidak dapat dideteksi oleh kompiler, kecuali bahasa tersebut memiliki pelacakan kepemilikan seperti Rust (dalam hal ini sudah ada solusi yang lebih baik untuk masalah manajemen sumber daya ini).

Ini meninggalkan satu-satunya solusi yang layak: manajemen sumber daya manual, mungkin dengan menerapkan penghitungan referensi sendiri. Ini rawan kesalahan, tetapi bukan tidak mungkin. Khususnya, harus memikirkan kepemilikan tidak biasa dalam bahasa GC, jadi kode yang ada mungkin tidak cukup eksplisit tentang jaminan kepemilikan.


3

Banyak informasi bagus dari jawaban yang lain.

Namun, untuk secara eksplisit, pola yang mungkin Anda cari adalah bahwa Anda menggunakan objek tunggal yang dimiliki tunggal untuk konstruk aliran kontrol seperti RAII via usingdan IDispose, dalam hubungannya dengan objek (lebih besar, mungkin referensi dihitung) yang memegang beberapa (operasi sumber daya sistem).

Jadi ada objek pemilik tunggal tunggal yang tidak dibagi-pakai yang (melalui objek yang lebih kecil IDisposedan usingkonstruk aliran kontrol) dapat menginformasikan objek bersama yang lebih besar (mungkin kustom Acquire& Releasemetode).

(Metode Acquiredan Releaseyang ditunjukkan di bawah ini kemudian juga tersedia di luar konstruksi menggunakan, tetapi tanpa keamanan yang trytersirat di using.)


Contoh dalam 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;
    }
}

Jika itu seharusnya C # (yang terlihat seperti) maka implementasi <T> Referensi Anda agak salah. Kontrak untuk IDisposable.Disposenegara yang memanggil Disposebeberapa kali pada objek yang sama harus menjadi no-op. Jika saya menerapkan pola seperti itu, saya juga akan membuat Releasepribadi untuk menghindari kesalahan yang tidak perlu dan menggunakan pendelegasian alih-alih warisan (menghapus antarmuka, menyediakan SharedDisposablekelas sederhana yang dapat digunakan dengan Disposables sewenang-wenang), tetapi itu lebih merupakan masalah selera.
Voo

@Vo, ok, poin bagus, terima kasih!
Erik Eidt

1

Sebagian besar objek dalam sistem umumnya harus sesuai dengan salah satu dari tiga pola:

  1. Objek yang keadaannya tidak akan pernah berubah, dan referensi yang dimiliki murni sebagai sarana merangkum negara. Entitas yang memegang referensi tidak tahu atau tidak peduli apakah entitas lain memegang referensi untuk objek yang sama.

  2. Objek yang berada di bawah kendali eksklusif entitas tunggal, yang merupakan pemilik tunggal dari semua status di dalamnya, dan menggunakan objek tersebut semata-mata sebagai sarana merangkum keadaan (mungkin bisa berubah) di dalamnya.

  3. Objek yang dimiliki oleh satu entitas, tetapi entitas lain yang diizinkan untuk digunakan secara terbatas. Pemilik objek dapat menggunakannya tidak hanya sebagai sarana enkapsulasi negara, tetapi juga mengenkapsulasi hubungan dengan entitas lain yang membagikannya.

Melacak pengumpulan sampah berfungsi lebih baik daripada penghitungan referensi untuk # 1, karena kode yang menggunakan objek seperti itu tidak perlu melakukan sesuatu yang istimewa ketika dilakukan dengan referensi yang tersisa. Penghitungan referensi tidak diperlukan untuk # 2 karena objek akan memiliki tepat satu pemilik, dan itu akan tahu kapan itu tidak lagi membutuhkan objek. Skenario # 3 dapat menimbulkan beberapa kesulitan jika pemilik objek membunuhnya sementara entitas lain masih memegang referensi; bahkan di sana, pelacakan GC mungkin lebih baik daripada penghitungan referensi untuk memastikan bahwa referensi ke objek mati tetap dapat diidentifikasi sebagai referensi ke objek mati, selama referensi tersebut ada.

Ada beberapa situasi di mana mungkin perlu memiliki objek tanpa pemilik yang dapat dibagikan untuk memperoleh dan memiliki sumber daya eksternal selama ada yang membutuhkan layanannya, dan harus melepaskannya ketika layanannya tidak lagi diperlukan. Sebagai contoh, sebuah objek yang merangkum konten file read-only dapat dibagikan dan digunakan oleh banyak entitas secara bersamaan tanpa ada yang harus tahu atau peduli tentang keberadaan masing-masing. Namun, keadaan seperti itu jarang terjadi. Sebagian besar objek akan memiliki pemilik tunggal yang jelas, atau tidak memiliki pemilik. Kepemilikan ganda dimungkinkan, tetapi jarang bermanfaat.


0

Kepemilikan Bersama Jarang Masuk Akal

Jawaban ini mungkin sedikit menyinggung, tetapi saya harus bertanya, berapa banyak kasus yang masuk akal dari sudut pandang pengguna-akhir untuk berbagi kepemilikan ? Setidaknya dalam domain tempat saya bekerja, hampir tidak ada domain karena jika tidak, itu berarti bahwa pengguna tidak perlu hanya menghapus sesuatu satu kali dari satu tempat, tetapi secara eksplisit menghapusnya dari semua pemilik yang relevan sebelum sumber daya sebenarnya dihapus dari sistem.

Ini sering merupakan ide rekayasa tingkat rendah untuk mencegah sumber daya tidak dihancurkan sementara sesuatu yang lain masih mengaksesnya, seperti utas lainnya. Seringkali ketika pengguna meminta untuk menutup / menghapus / menghapus sesuatu dari perangkat lunak, itu harus dihapus sesegera mungkin (setiap kali aman untuk dihapus), dan itu pasti tidak boleh berlama-lama dan menyebabkan kebocoran sumber daya selama aplikasi sedang berjalan.

Sebagai contoh, aset gim dalam gim video mungkin merujuk materi dari pustaka materi. Kami tentu saja tidak ingin, misalnya, crash pointer menggantung jika materi dihapus dari perpustakaan materi dalam satu utas sementara utas lainnya masih mengakses materi yang dirujuk oleh aset game. Tapi itu tidak berarti masuk akal bagi aset game untuk berbagi kepemilikan materi yang mereka rujuk dengan perpustakaan materi. Kami tidak ingin memaksa pengguna untuk menghapus materi secara eksplisit dari pustaka aset dan material. Kami hanya ingin memastikan bahwa bahan tidak dihapus dari perpustakaan materi, satu-satunya pemilik bahan yang masuk akal, sampai utas lainnya selesai mengakses materi.

Kebocoran Sumber Daya

Namun saya bekerja dengan mantan tim yang merangkul GC untuk semua komponen dalam perangkat lunak. Dan sementara itu benar-benar membantu memastikan kami tidak pernah menghancurkan sumber daya sementara utas lainnya masih mengaksesnya, kami malah mendapatkan bagian kami dari kebocoran sumber daya .

Dan ini bukan kebocoran sumber daya sepele dari jenis yang hanya mengganggu pengembang, seperti kilobyte memori yang bocor setelah sesi selama satu jam. Ini adalah kebocoran epik, sering gigabyte memori selama sesi aktif, yang mengarah ke laporan bug. Karena sekarang ketika kepemilikan sumber daya sedang direferensikan (dan karenanya dibagikan kepemilikan) di antara, katakanlah, 8 bagian sistem yang berbeda, maka hanya diperlukan satu untuk gagal menghapus sumber daya sebagai tanggapan terhadap pengguna yang meminta agar dihapus untuk itu. menjadi bocor dan mungkin tanpa batas.

Jadi saya tidak pernah menjadi penggemar berat GC atau penghitungan referensi yang diterapkan pada skala luas karena betapa mudahnya mereka membuat perangkat lunak yang bocor. Apa yang tadinya adalah crash pointer menjuntai yang mudah dideteksi berubah menjadi kebocoran sumber daya yang sangat sulit dideteksi yang dapat dengan mudah terbang di bawah radar pengujian.

Referensi yang lemah dapat mengurangi masalah ini jika bahasa / pustaka menyediakan ini, tetapi saya merasa sulit untuk mendapatkan tim pengembang wajan campuran untuk dapat secara konsisten menggunakan referensi yang lemah kapan pun sesuai. Dan kesulitan ini tidak hanya terkait dengan tim internal, tetapi untuk setiap pengembang plugin tunggal untuk perangkat lunak kami. Mereka juga dapat dengan mudah menyebabkan sistem membocorkan sumber daya dengan hanya menyimpan referensi terus-menerus ke suatu objek dengan cara yang membuatnya sulit untuk melacak kembali ke plugin sebagai pelakunya, jadi kami juga mendapat bagian terbesar dari laporan bug yang dihasilkan dari sumber daya perangkat lunak kami bocor hanya karena plugin yang kode sumbernya di luar kendali kami gagal merilis referensi ke sumber daya yang mahal itu.

Solusi: Ditangguhkan, Penghapusan Berkala

Jadi solusi saya nanti yang saya terapkan pada proyek pribadi saya yang memberi saya jenis yang terbaik yang saya temukan dari kedua dunia adalah untuk menghilangkan konsep itu referencing=ownershiptetapi masih menunda kerusakan sumber daya.

Akibatnya, sekarang setiap kali pengguna melakukan sesuatu yang menyebabkan sumber daya perlu dihapus, API dinyatakan dalam hal hanya menghapus sumber daya:

ecs->remove(component);

... yang memodelkan logika pengguna-akhir dengan cara yang sangat mudah. Namun, sumber daya (komponen) tidak dapat dihapus segera jika ada utas sistem lainnya dalam fase pemrosesan mereka di mana mereka dapat mengakses komponen yang sama secara bersamaan.

Jadi utas pemrosesan ini kemudian menghasilkan waktu di sana-sini yang memungkinkan utas yang menyerupai pengumpul sampah untuk bangun dan " menghentikan dunia " dan menghancurkan semua sumber daya yang diminta untuk dihapus sambil mengunci keluar benang dari memproses komponen-komponen tersebut sampai selesai . Saya telah menyetel ini sehingga jumlah pekerjaan yang perlu dilakukan di sini pada umumnya minimal dan tidak memangkas laju bingkai.

Sekarang saya tidak bisa mengatakan ini adalah metode yang telah dicoba dan diuji dan didokumentasikan dengan baik, tetapi ini adalah sesuatu yang telah saya gunakan selama beberapa tahun sekarang tanpa sakit kepala sama sekali dan tidak ada kebocoran sumber daya. Saya sarankan mengeksplorasi pendekatan seperti ini ketika arsitektur Anda mungkin cocok dengan model konkurensi seperti ini karena jauh lebih ringan daripada GC atau penghitungan ulang dan tidak mengambil risiko kebocoran sumber daya jenis ini terbang di bawah radar pengujian.

Satu tempat di mana saya menemukan penghitungan ulang atau GC berguna adalah untuk struktur data yang persisten. Dalam hal ini adalah wilayah struktur data, jauh terpisah dari masalah pengguna akhir, dan di sana sebenarnya masuk akal untuk setiap salinan yang tidak berubah untuk berpotensi berbagi kepemilikan atas data yang tidak dimodifikasi yang sama.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.