IDisposable - bahwa ibumu tidak berbicara tentang membebaskan sumber daya. Bagian 1

Ini adalah terjemahan dari bagian pertama artikel. Artikel ini ditulis pada tahun 2008. Setelah 10 tahun, hampir kehilangan relevansinya.


Pelepasan Sumber Daya Deterministik - Suatu Kebutuhan


Selama lebih dari 20 tahun pengalaman pengkodean, saya terkadang mengembangkan bahasa saya sendiri untuk menyelesaikan masalah. Mereka berkisar dari bahasa sederhana, imperatif hingga ekspresi reguler khusus untuk pohon. Saat membuat bahasa, ada banyak rekomendasi dan beberapa aturan sederhana tidak boleh dilanggar. Salah satunya:


Jangan pernah membuat bahasa pengecualian di mana tidak ada rilis sumber daya deterministik.

Tebak rekomendasi apa yang tidak diikuti oleh .NET runtime, dan sebagai hasilnya, semua bahasa didasarkan padanya?


Alasan aturan ini ada adalah bahwa pelepasan sumber daya deterministik diperlukan untuk membuat program yang didukung . Pelepasan sumber daya yang ditentukan memberikan titik tertentu di mana programmer yakin bahwa sumber daya itu dibebaskan. Ada dua cara untuk menulis program yang andal: pendekatan tradisional adalah melepaskan sumber daya sedini mungkin dan pendekatan modern adalah melepaskan sumber daya untuk waktu yang tidak ditentukan. Keuntungan dari pendekatan modern adalah bahwa programmer tidak perlu secara eksplisit membebaskan sumber daya. Kerugiannya adalah bahwa jauh lebih sulit untuk menulis aplikasi yang dapat diandalkan, ada banyak kesalahan halus. Sayangnya, .NET runtime dibuat menggunakan pendekatan modern.


.NET mendukung pelepasan sumber daya non-deterministik menggunakan metode Finalize , yang memiliki arti khusus. Untuk rilis sumber daya deterministik, Microsoft juga menambahkan antarmuka IDisposable (dan kelas lainnya, yang akan kita bahas nanti). Namun demikian, untuk runtime IDisposable adalah antarmuka normal, seperti yang lainnya. Status "kelas dua" ini menciptakan beberapa kesulitan.


Dalam C #, "rilis deterministik untuk orang miskin" dapat diimplementasikan menggunakan try dan finally try atau using (yang merupakan hal yang hampir sama). Microsoft telah berdiskusi untuk waktu yang lama apakah akan menghitung jumlah tautan atau tidak, dan menurut saya keputusan yang salah telah dibuat. Akibatnya, untuk pelepasan sumber daya yang deterministik, Anda harus using konstruksi yang canggung finally \ using atau panggilan langsung ke IDisposable.Dispose , yang penuh dengan kesalahan. Untuk seorang programmer C ++ yang terbiasa menggunakan shared_ptr<T> kedua opsi tidak menarik. (Kalimat terakhir memperjelas di mana penulis memiliki hubungan seperti itu - kira - kira.


IDisposable


IDisposable adalah solusi untuk pelepasan sumber daya deterministik yang ditawarkan oleh Misoftro. Salah satunya adalah untuk kasus-kasus berikut:


  • Jenis apa pun yang memiliki sumber daya yang dikelola ( IDisposable ). Suatu tipe harus tentu memiliki , yaitu, mengatur waktu hidup, sumber daya, dan tidak hanya merujuk kepada mereka.
  • Jenis apa pun yang memiliki sumber daya yang tidak dikelola.
  • Jenis apa pun yang memiliki sumber daya yang dikelola dan tidak dikelola.
  • Jenis apa pun yang diwarisi dari kelas yang mengimplementasikan IDisposable . Saya tidak merekomendasikan mewarisi dari kelas yang memiliki sumber daya yang tidak dikelola. Lebih baik gunakan lampiran.

IDisposable membantu membebaskan sumber daya secara deterministik, tetapi memiliki masalah sendiri.


Kesulitan IDisposable - Kegunaan


Objek IDisposable digunakan cukup rumit. Menggunakan objek harus dibungkus using konstruksi. Berita buruknya adalah bahwa C # tidak mengizinkan using dengan tipe yang tidak menerapkan IDisposable . Oleh karena itu, programmer harus merujuk pada dokumentasi setiap kali untuk memahami apakah perlu menulis using , atau hanya menulis using mana using mana, dan kemudian menghapus di mana compiler bersumpah.


Managed C ++ jauh lebih baik dalam hal ini. Ini mendukung semantik tumpukan untuk jenis referensi , yang berfungsi hanya using untuk jenis yang diperlukan. C # dapat mengambil manfaat dari kemampuan menulis using jenis apa pun.


Masalah ini dapat diatasi dengan. alat analisis kode. Untuk membuat keadaan menjadi lebih buruk, jika Anda lupa menggunakan, program dapat lulus tes, tetapi crash saat bekerja "di ladang."


Alih-alih menghitung tautan, IDisposable memiliki masalah lain - menentukan pemilik. Ketika di C ++ salinan terakhir dari shared_ptr<T> keluar dari ruang lingkup, sumber daya dibebaskan segera, tidak perlu berpikir siapa yang harus bebas. IDisposable sebaliknya, memaksa programmer untuk menentukan siapa yang "memiliki" objek dan bertanggung jawab untuk melepaskannya. Terkadang kepemilikan jelas: ketika satu objek merangkum objek lain dan itu sendiri mengimplementasikan IDisposable , oleh karena itu bertanggung jawab atas pelepasan objek anak. Terkadang umur suatu objek ditentukan oleh satu blok kode, dan programmer hanya menggunakan di sekitar blok ini. Namun demikian, ada banyak kasus di mana suatu objek dapat digunakan di beberapa tempat dan masa hidupnya sulit untuk ditentukan (walaupun dalam kasus ini jumlah referensi akan baik-baik saja).


IDisposable Kesulitan - Kompatibilitas Mundur


Menambahkan IDisposable ke kelas dan menghapus IDisposable dari daftar antarmuka yang diimplementasikan merupakan perubahan besar. Kode klien yang tidak mengharapkan IDisposable tidak akan membebaskan sumber daya jika Anda menambahkan IDisposable ke salah satu kelas Anda yang diteruskan dengan referensi ke antarmuka atau kelas dasar.


Microsoft sendiri mengalami masalah ini. IEnumerator tidak diwarisi dari IDisposable , dan IEnumerator<T> diwarisi. Jika Anda meneruskan IEnumerator<T> kode yang menerima IEnumerator , Dispose tidak akan dipanggil.


Ini bukan akhir dunia, tetapi memberikan beberapa esensi sekunder IDisposable .


Kesulitan IDisposable - Merancang Hirarki Kelas


Kelemahan terbesar yang disebabkan oleh IDisposable di bidang desain hierarki adalah bahwa setiap kelas dan antarmuka harus memprediksi apakah IDisposable akan dibutuhkan oleh turunannya.


Jika antarmuka tidak mewarisi IDisposable , tetapi kelas yang mengimplementasikan antarmuka juga menerapkan IDisposable , maka kode akhir akan mengabaikan rilis deterministik, atau harus memeriksa apakah objek mengimplementasikan antarmuka IDisposable . Tetapi untuk ini, tidak mungkin menggunakan konstruk penggunaan dan Anda harus menulis try jelek dan finally .


Singkatnya, IDisposable mempersulit pengembangan perangkat lunak yang dapat digunakan kembali. Alasan utamanya adalah pelanggaran terhadap salah satu prinsip desain berorientasi objek - pemisahan antarmuka dan implementasi. Rilis sumber daya harus merupakan detail implementasi. Microsoft memutuskan untuk membuat rilis sumber daya deterministik antarmuka kelas dua.


Salah satu solusi yang tidak begitu indah adalah membuat semua kelas mengimplementasikan IDisposable , tetapi di sebagian besar kelas, IDisposable.Dispose tidak akan melakukan apa pun. Tapi ini tidak terlalu cantik.


Kesulitan lain dengan IDisposable adalah koleksi. Beberapa koleksi "memiliki" objek di dalamnya, dan beberapa tidak. Namun, koleksi itu sendiri tidak menerapkan IDisposable . Programmer harus ingat untuk memanggil IDisposable.Dispose objek dalam koleksi, atau buat turunannya sendiri dari kelas koleksi yang mengimplementasikan IDisposable berarti kepemilikan.


IDisposable Kesulitan - tambahan status "salah"


IDisposable dapat dipanggil secara eksplisit kapan saja, terlepas dari masa pakai objek. Artinya, keadaan "dirilis" ditambahkan ke setiap objek, di mana dianjurkan untuk melempar ObjectDisposedException . Memeriksa status dan melempar pengecualian adalah biaya tambahan.


Alih-alih memeriksa setiap bersin, lebih baik mempertimbangkan mengakses objek dalam keadaan "bebas" sebagai "perilaku tidak terdefinisi" sebagai panggilan ke memori yang dibebaskan.


Kesulitan IDisposable - tidak ada jaminan


IDisposable hanyalah sebuah antarmuka. Kelas yang mengimplementasikan IDisposable mendukung rilis deterministik, tetapi tidak menjaminnya . Untuk kode klien, tidak masalah untuk tidak memanggil Dispose . Oleh karena itu, kelas yang mengimplementasikan IDisposable harus mendukung rilis deterministik dan non-deterministik.


Complexities IDisposable - Implementasi Kompleks


Microsoft menawarkan pola penerapan IDisposable . (Sebelumnya, ada pola yang secara umum mengerikan, tetapi relatif baru-baru ini, setelah kemunculan .NET 4, dokumentasinya diperbaiki, termasuk di bawah pengaruh artikel ini. Dalam edisi lama buku .NET, Anda dapat menemukan versi yang lama. - perkiraan )


  • IDisposable.Dispose mungkin tidak dipanggil sama sekali, sehingga kelas harus menyertakan finalizer ke sumber daya gratis.
  • IDisposable.Dispose bisa dipanggil beberapa kali dan harus bekerja tanpa efek samping yang terlihat. Oleh karena itu, perlu untuk menambahkan verifikasi apakah metode tersebut telah dipanggil atau belum.
  • Finalizers dipanggil dalam utas terpisah dan dapat dipanggil sebelum IDisposable.Dispose . IDisposable.Dispose . Penggunaan GC.SuppressFinalize untuk menghindari "balapan" tersebut.

Selain itu:


  • Finalizers dipanggil, termasuk untuk objek yang melempar pengecualian pada konstruktor. Oleh karena itu, kode rilis harus bekerja dengan objek yang diinisialisasi sebagian.
  • Menerapkan IDisposable di kelas yang diwarisi dari CriticalFinalizerObject membutuhkan konstruksi non-sepele. void Dispose(bool disposing) adalah metode viral dan harus dieksekusi di Wilayah Eksekusi Terbatas , yang mengharuskan panggilan ke RuntimeHelpers.PrepareMethod .

Kesulitan IDisposable - Tidak Cocok untuk Penyelesaian Logika


Mematikan suatu objek - sering terjadi dalam program-program dalam urutan paralel atau asinkron. Misalnya, kelas menggunakan utas terpisah dan ingin melengkapinya menggunakan ManualResetEvent . Ini dapat dilakukan di IDisposable.Dispose , tetapi dapat menyebabkan kesalahan jika kode dipanggil di finalizer.


Untuk memahami batasan dalam finalizer, Anda perlu memahami cara kerja pemulung. Di bawah ini adalah diagram yang disederhanakan di mana banyak detail terkait dengan generasi, tautan lemah, pemulihan objek, pengumpulan sampah latar belakang, dll. Dihilangkan.


Pengumpul sampah .NET menggunakan algoritma mark-and-sweep. Secara umum, logikanya terlihat seperti ini:


  1. Jeda semua utas.
  2. Ambil semua objek root: variabel pada stack, bidang statis, objek GCHandle , antrian finalisasi. Dalam kasus pembongkaran domain aplikasi (penghentian program), dianggap bahwa variabel dalam stack dan bidang statis bukanlah root.
  3. Secara rekursif menelusuri semua tautan dari objek dan menandainya sebagai "terjangkau".
  4. Pergi melalui semua objek lain yang memiliki destruktor (finalizer), menyatakan mereka dapat dijangkau, dan menempatkan mereka dalam antrian finalisasi ( GC.SuppressFinalize memberitahu GC untuk tidak melakukan ini). Objek diantrekan dalam urutan yang tidak terduga.

Di latar belakang, aliran (atau beberapa) finalisasi berfungsi:


  1. Mengambil objek dari antrian dan mulai menyelesaikannya. Dimungkinkan untuk menjalankan beberapa finalizer dari objek yang berbeda secara bersamaan.
  2. Objek dihapus dari antrian, dan jika tidak ada orang lain yang merujuknya, itu akan dihapus pada pengumpulan sampah berikutnya.

Sekarang harus jelas mengapa tidak mungkin untuk mengakses sumber daya yang dikelola dari finalizer - Anda tidak tahu dalam urutan apa disebut finalizer. Bahkan memanggil IDisposable.Dispose objek lain dari finalizer dapat menyebabkan kesalahan, karena kode rilis sumber daya dapat bekerja di utas lainnya.


Ada beberapa pengecualian ketika Anda dapat mengakses sumber daya yang dikelola dari finalizer:


  1. Finalisasi objek yang diwarisi dari CriticalFinalizerObject dilakukan setelah finalisasi objek yang tidak diwarisi dari kelas ini. Ini berarti Anda bisa memanggil ManualResetEvent dari finalizer hingga kelas diwarisi dari CriticalFinalizerObject
  2. Beberapa objek dan metode khusus, seperti Konsol dan beberapa metode Thread. Mereka dapat dipanggil dari finalizers bahkan jika program berakhir.

Dalam kasus umum, lebih baik tidak mengakses sumber daya yang dikelola dari finalizer. Namun demikian, logika penyelesaian diperlukan untuk perangkat lunak non-sepele. Pada Windows.Forms berisi logika penyelesaian dalam metode Application.Exit . Saat Anda mengembangkan pustaka komponen Anda, hal terbaik yang harus dilakukan adalah menyelesaikan logika penyelesaian dengan IDisposable . Pengakhiran normal jika terjadi panggilan IDisposable.Dispose dan darurat jika tidak.


Microsoft juga mengalami masalah ini. Kelas StreamWriter memiliki objek Stream (tergantung pada parameter konstruktor di versi terbaru - sekitar Per. ). StreamWriter.Close flushes buffer dan panggilan Stream.Close (juga terjadi jika dibungkus using - kira - kira Per. ). Jika StreamWriter tidak ditutup, buffer tidak memerah dan obrolan data hilang. Microsoft tidak mendefinisikan ulang finalizer, sehingga "menyelesaikan" masalah penyelesaian. Contoh yang bagus tentang perlunya logika penyelesaian.


Saya sarankan membaca


Banyak informasi tentang .NET internal dalam artikel ini berasal dari CLR Jeffrey Richter via C #. Jika Anda belum memilikinya, maka belilah . Serius. Ini adalah pengetahuan yang diperlukan untuk setiap programmer C #.


Kesimpulan dari penerjemah


Sebagian besar .NET programmer tidak akan pernah menemui masalah yang dijelaskan dalam artikel ini. .NET akan berevolusi untuk meningkatkan tingkat abstraksi dan mengurangi kebutuhan akan "juggling" sumber daya yang tidak dikelola. Namun demikian, artikel ini bermanfaat karena menjelaskan detail mendalam dari hal-hal sederhana dan dampaknya pada desain kode.


Bagian selanjutnya akan menjadi diskusi terperinci tentang cara bekerja dengan sumber daya yang dikelola dan tidak dikelola di .NET dengan banyak contoh.

Source: https://habr.com/ru/post/id414873/


All Articles