
Multithreading
Sekarang mari kita bicara tentang es tipis. Pada bagian sebelumnya tentang IDisposable kami menyentuh satu konsep yang sangat penting yang mendasari tidak hanya prinsip-prinsip desain tipe Disposable tetapi semua tipe pada umumnya. Ini adalah konsep integritas objek. Ini berarti bahwa pada suatu waktu tertentu suatu objek berada dalam keadaan yang ditentukan secara ketat dan tindakan apa pun dengan objek ini mengubah kondisinya menjadi salah satu opsi yang telah ditentukan sebelumnya saat merancang jenis objek ini. Dengan kata lain, tidak ada tindakan dengan objek yang dapat mengubahnya menjadi keadaan yang tidak ditentukan. Ini menghasilkan masalah dengan tipe yang dirancang dalam contoh di atas. Mereka bukan thread-safe. Ada kemungkinan metode publik jenis ini akan dipanggil ketika penghancuran objek sedang berlangsung. Mari kita selesaikan masalah ini dan putuskan apakah kita harus menyelesaikannya sama sekali.
Bab ini diterjemahkan dari bahasa Rusia bersama oleh penulis dan penerjemah profesional . Anda dapat membantu kami dengan terjemahan dari bahasa Rusia atau Inggris ke bahasa lain, terutama ke bahasa Cina atau Jerman.
Juga, jika Anda ingin berterima kasih kepada kami, cara terbaik yang dapat Anda lakukan adalah memberi kami bintang di github atau untuk repositori garpu
github / sidristij / dotnetbook .
public class FileWrapper : IDisposable { IntPtr _handle; bool _disposed; object _disposingSync = new object(); public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Seek(int position) { lock(_disposingSync) { CheckDisposed(); // Seek API call } } public void Dispose() { lock(_disposingSync) { if(_disposed) return; _disposed = true; } InternalDispose(); GC.SuppressFinalize(this); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { lock(_disposingSync) { if(_disposed) { throw new ObjectDisposedException(); } } } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods }
Kode validasi _disposed
dalam Buang () harus diinisialisasi sebagai bagian kritis. Bahkan, seluruh kode metode publik harus diinisialisasi sebagai bagian penting. Ini akan memecahkan masalah akses bersamaan ke metode publik dari jenis instance dan ke metode penghancurannya. Namun, itu membawa masalah lain yang menjadi bom waktu:
- Penggunaan intensif metode instance type serta penciptaan dan penghancuran objek akan menurunkan kinerja secara signifikan. Ini karena mengambil kunci menghabiskan waktu. Waktu ini diperlukan untuk mengalokasikan tabel SyncBlockIndex, memeriksa utas saat ini dan banyak hal lainnya (kita akan membahasnya dalam bab tentang multithreading). Itu berarti kita harus mengorbankan kinerja objek selama masa hidupnya untuk "jarak terakhir" hidupnya.
- Lalu lintas memori tambahan untuk objek sinkronisasi.
- Langkah-langkah tambahan yang harus diambil GC untuk melalui grafik objek.
Sekarang, beri nama yang kedua dan, menurut saya, yang paling penting. Kami mengizinkan penghancuran suatu objek dan pada saat yang sama berharap untuk bekerja dengannya lagi. Apa yang kita harapkan dalam situasi ini? bahwa itu akan gagal? Karena jika Buang berjalan terlebih dahulu, maka penggunaan metode objek berikut ini pasti akan menghasilkan ObjectDisposedException
. Jadi, Anda harus mendelegasikan sinkronisasi antara panggilan Buang () dan metode publik lainnya dari jenis ke sisi layanan, yaitu ke kode yang membuat instance kelas FileWrapper
. Itu karena hanya sisi penciptaan yang tahu apa yang akan dilakukannya dengan instance kelas dan kapan harus menghancurkannya. Di sisi lain, panggilan Buang harus menghasilkan hanya kesalahan kritis, seperti OutOfMemoryException
, tetapi bukan IOException misalnya. Ini karena persyaratan untuk arsitektur kelas yang mengimplementasikan IDisposable. Ini berarti bahwa jika Buang dipanggil dari lebih dari satu utas pada satu waktu, penghancuran suatu entitas dapat terjadi dari dua utas secara bersamaan (kami melewatkan pemeriksaan if(_disposed) return;
). Itu tergantung pada situasinya: jika sumber daya dapat dilepaskan beberapa kali, tidak perlu ada pemeriksaan tambahan. Kalau tidak, perlindungan diperlukan:
// I don't show the whole pattern on purpose as the example will be too long // and will not show the essence class Disposable : IDisposable { private volatile int _disposed; public void Dispose() { if(Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) { // dispose } } }
Dua tingkat Prinsip Desain Sekali Pakai
Apa pola paling populer untuk mengimplementasikan IDisposable
yang dapat Anda temui di .NET books dan Internet? Pola apa yang diharapkan dari Anda selama wawancara untuk pekerjaan baru yang potensial? Kemungkinan besar yang ini:
public class Disposable : IDisposable { bool _disposed; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if(disposing) { // here we release managed resources } // here we release unmanaged resources } protected void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } ~Disposable() { Dispose(false); } }
Apa yang salah dengan contoh ini dan mengapa kami belum menulis seperti ini sebelumnya? Faktanya, ini adalah pola yang cocok untuk semua situasi. Namun, penggunaannya di mana-mana bukanlah gaya yang baik menurut saya karena kita hampir tidak berurusan dengan sumber daya yang tidak dikelola dalam praktik yang membuat setengah dari pola tidak ada gunanya. Selain itu, karena secara bersamaan mengelola sumber daya yang dikelola dan tidak dikelola, itu melanggar prinsip pembagian tanggung jawab. Saya pikir ini salah. Mari kita lihat pendekatan yang sedikit berbeda. Prinsip Desain Sekali Pakai . Secara singkat, ini berfungsi sebagai berikut:
Membuang dibagi menjadi dua tingkatan kelas:
- Tipe Level 0 secara langsung merangkum sumber daya yang tidak dikelola
- Mereka abstrak atau dikemas.
- Semua metode harus ditandai:
- PrePrepareMethod, sehingga suatu metode dapat dikompilasi saat memuat suatu tipe
- SecuritySafeCritical untuk melindungi terhadap panggilan dari kode, bekerja di bawah batasan
- ReliabilityContract (Consistency.WillNotCorruptState, Cer.Success / MayFail)] untuk meletakkan CER untuk suatu metode dan semua panggilan anaknya
- Mereka dapat mereferensikan tipe Level 0, tetapi harus menambah counter objek referensi untuk menjamin urutan yang tepat untuk memasuki "jarak terakhir"
- Tipe Level 1 merangkum hanya sumber daya yang dikelola
- Mereka hanya diwarisi dari tipe Level 1 atau langsung menerapkan IDisposable
- Mereka tidak dapat mewarisi tipe Level 0 atau CriticalFinalizerObject
- Mereka dapat merangkum tipe terkelola Level 1 dan Level 0
- Mereka menerapkan IDisposable. Buang dengan menghancurkan objek yang dienkapsulasi mulai dari tipe Level 0 dan pergi ke Level 1
- Mereka tidak menerapkan finalizer karena mereka tidak berurusan dengan sumber daya yang tidak dikelola
- Mereka harus berisi properti yang dilindungi yang memberikan akses ke tipe Level 0.
Itulah mengapa saya menggunakan divisi menjadi dua jenis dari awal: yang berisi sumber daya yang dikelola dan yang dengan sumber daya yang tidak dikelola. Mereka harus berfungsi secara berbeda.
Cara lain untuk menggunakan Buang
Gagasan di balik penciptaan IDisposable adalah untuk melepaskan sumber daya yang tidak dikelola. Tetapi seperti banyak pola lainnya, sangat membantu untuk tugas-tugas lain, misalnya untuk melepaskan referensi ke sumber daya yang dikelola. Meskipun melepaskan sumber daya yang dikelola tidak terdengar sangat membantu. Maksud saya mereka dipanggil dikelola dengan sengaja sehingga kita akan bersantai dengan senyum tentang pengembang C / C ++, kan? Namun, tidak demikian. Mungkin selalu ada situasi di mana kita kehilangan referensi ke suatu objek tetapi pada saat yang sama berpikir bahwa semuanya baik-baik saja: GC akan mengumpulkan sampah, termasuk objek kita. Namun, ternyata ingatan itu bertambah. Kami masuk ke program analisis memori dan melihat bahwa ada sesuatu yang memegang objek ini. Masalahnya adalah bahwa mungkin ada logika untuk menangkap secara implisit referensi ke entitas Anda di kedua platform .NET dan arsitektur kelas eksternal. Karena penangkapannya tersirat, seorang programmer dapat kehilangan kebutuhan akan rilisnya dan kemudian mendapatkan kebocoran memori.
Delegasi, acara
Mari kita lihat contoh sintetis ini:
class Secondary { Action _action; void SaveForUseInFuture(Action action) { _action = action; } public void CallAction() { _action(); } } class Primary { Secondary _foo = new Secondary(); public void PlanSayHello() { _foo.SaveForUseInFuture(Strategy); } public void SayHello() { _foo.CallAction(); } void Strategy() { Console.WriteLine("Hello!"); } }
Masalah apa yang ditunjukkan oleh kode ini? Toko kelas kedua mendelegasikan tipe Action
di bidang _action
yang diterima dalam metode SaveForUseInFuture
. Selanjutnya, metode PlanSayHello
di dalam kelas Primary
melewati pointer ke metode Strategy
ke kelas Secondary
. Sangat mengherankan, tetapi jika, dalam contoh ini, Anda melewati suatu metode statis atau metode instance, SaveForUseInFuture
disahkan tidak akan diubah, tetapi instance kelas Primary
akan direferensikan secara implisit atau tidak direferensikan sama sekali. Dari luar sepertinya Anda menginstruksikan metode mana yang harus dihubungi. Namun pada kenyataannya, sebuah delegasi dibangun tidak hanya menggunakan pointer metode tetapi juga menggunakan pointer ke instance kelas. Pihak yang menelepon harus memahami untuk instance kelas apa yang harus memanggil metode Strategy
! Itu adalah instance dari kelas Secondary
telah secara implisit menerima dan memegang pointer ke instance dari kelas Primary
, meskipun tidak ditunjukkan secara eksplisit. Bagi kami itu hanya berarti bahwa jika kita melewati pointer _foo
tempat lain dan kehilangan referensi ke Primary
, maka GC tidak akan mengumpulkan objek Primary
, karena Secondary
akan menahannya. Bagaimana kita bisa menghindari situasi seperti itu? Kami membutuhkan pendekatan yang ditentukan untuk merilis referensi kepada kami. Sebuah mekanisme yang sangat cocok dengan tujuan ini dapat digunakan IDisposable
// This is a simplified implementation class Secondary : IDisposable { Action _action; public event Action<Secondary> OnDisposed; public void SaveForUseInFuture(Action action) { _action = action; } public void CallAction() { _action?.Invoke(); } void Dispose() { _action = null; OnDisposed?.Invoke(this); } }
Sekarang contohnya terlihat dapat diterima. Jika instance kelas dilewatkan ke pihak ketiga dan referensi ke delegasi _action
akan hilang selama proses ini, kami akan menetapkannya menjadi nol dan pihak ketiga akan diberitahu tentang penghancuran instance dan menghapus referensi untuk itu .
Bahaya kedua kode yang dijalankan pada delegasi adalah prinsip-prinsip event
berfungsi. Mari kita lihat apa hasilnya:
// a private field of a handler private Action<Secondary> _event; // add/remove methods are marked as [MethodImpl(MethodImplOptions.Synchronized)] // that is similar to lock(this) public event Action<Secondary> OnDisposed { add { lock(this) { _event += value; } } remove { lock(this) { _event -= value; } } }
C # messaging menyembunyikan internal peristiwa dan menyimpan semua objek yang berlangganan untuk memperbarui melalui event
. Jika terjadi kesalahan, referensi ke objek yang ditandatangani tetap di OnDisposed
dan akan menahan objek. Ini adalah situasi yang aneh karena dari segi arsitektur kita mendapatkan konsep "sumber acara" yang tidak boleh menyimpan apa pun secara logis. Namun pada kenyataannya, objek yang berlangganan pembaruan diadakan secara implisit. Selain itu, kami tidak dapat mengubah sesuatu di dalam array delegasi ini meskipun entitas itu milik kami. Satu-satunya hal yang dapat kita lakukan adalah menghapus daftar ini dengan menetapkan nol ke sumber acara.
Cara kedua adalah menerapkan metode add
/ remove
secara eksplisit, sehingga kami dapat mengontrol koleksi delegasi.
Situasi implisit lain mungkin muncul di sini. Tampaknya jika Anda menetapkan nol untuk sumber acara, berlangganan acara berikut akan menyebabkan NullReferenceException
. Saya pikir ini akan lebih logis.
Namun, ini tidak benar. Jika kode eksternal berlangganan acara setelah sumber acara dihapus, FCL akan membuat instance baru dari kelas Action dan menyimpannya di OnDisposed
. Implisit ini dalam C # dapat menyesatkan seorang programmer: berurusan dengan bidang yang dibatalkan harus menghasilkan semacam kewaspadaan daripada ketenangan. Di sini kami juga menunjukkan pendekatan ketika kecerobohan seorang programmer dapat menyebabkan kebocoran memori.
Penutupan Lambdas
Menggunakan gula sintaksis seperti lambda sangat berbahaya.
Saya ingin menyentuh gula sintaksis secara keseluruhan. Saya pikir Anda harus menggunakannya dengan hati-hati dan hanya jika Anda tahu hasilnya dengan tepat. Contoh dengan ekspresi lambda adalah penutupan, penutupan dalam Ekspresi dan banyak kesengsaraan lain yang dapat Anda lakukan pada diri Anda sendiri.
Tentu saja, Anda bisa mengatakan Anda tahu bahwa ekspresi lambda menciptakan penutupan dan dapat mengakibatkan risiko kebocoran sumber daya. Tapi itu sangat rapi, sangat menyenangkan sehingga sulit untuk menghindari menggunakan lambda daripada mengalokasikan seluruh metode, yang akan dijelaskan di tempat yang berbeda dari tempat akan digunakan. Sebenarnya, Anda tidak boleh ikut serta dalam provokasi ini, meskipun tidak semua orang bisa menolak. Mari kita lihat contohnya:
button.Clicked += () => service.SendMessageAsync(MessageType.Deploy);
Setuju, garis ini terlihat sangat aman. Tapi itu menyembunyikan masalah besar: sekarang button
variabel secara implisit merujuk service
dan menahannya. Bahkan jika kita memutuskan bahwa kita tidak membutuhkan service
lagi, button
masih akan menyimpan referensi saat variabel ini aktif. Salah satu cara untuk mengatasi masalah ini adalah dengan menggunakan pola untuk membuat IDisposable
dari Action
apa pun ( System.Reactive.Disposables
):
// Here we create a delegate from a lambda Action action = () => service.SendMessageAsync(MessageType.Deploy); // Here we subscribe button.Clicked += action; // We unsubscribe var subscription = Disposable.Create(() => button.Clicked -= action); // where it is necessary subscription.Dispose();
Akui, ini terlihat agak panjang dan kita kehilangan seluruh tujuan menggunakan ekspresi lambda. Jauh lebih aman dan sederhana untuk menggunakan metode pribadi yang umum untuk menangkap variabel secara implisit.
Perlindungan threadabort
Saat Anda membuat perpustakaan untuk pengembang pihak ketiga, Anda tidak bisa memprediksi perilakunya dalam aplikasi pihak ketiga. Terkadang Anda hanya bisa menebak apa yang dilakukan oleh seorang programmer terhadap perpustakaan Anda yang menyebabkan hasil tertentu. Salah satu contoh berfungsi di lingkungan multithreaded ketika konsistensi pembersihan sumber daya dapat menjadi masalah kritis. Perhatikan bahwa saat kami menulis metode Dispose()
, kami dapat menjamin tidak adanya pengecualian. Namun, kami tidak dapat memastikan bahwa saat menjalankan metode Dispose()
tidak akan ada ThreadAbortException
yang menonaktifkan utas eksekusi kami. Di sini kita harus ingat bahwa ketika ThreadAbortException
terjadi, semua blok catch / akhirnya dieksekusi (pada akhir catch / akhirnya block ThreadAbort terjadi lebih jauh). Jadi, untuk memastikan eksekusi kode tertentu dengan menggunakan Thread.Abort, Anda perlu membungkus bagian kritis di try { ... } finally { ... }
, lihat contoh di bawah ini:
void Dispose() { if(_disposed) return; _someInstance.Unsubscribe(this); _disposed = true; }
Seseorang dapat membatalkan ini kapan saja menggunakan Thread.Abort
. Ini sebagian menghancurkan objek, meskipun Anda masih bisa bekerja dengannya di masa depan. Pada saat yang sama, kode berikut:
void Dispose() { if(_disposed) return; // ThreadAbortException protection try {} finally { _someInstance.Unsubscribe(this); _disposed = true; } }
dilindungi dari pembatalan tersebut dan akan berjalan dengan lancar dan pasti, bahkan jika Thread.Abort
muncul di antara memanggil metode Unsubscribe
dan menjalankan instruksinya.
Hasil
Keuntungan
Ya, kami belajar banyak tentang pola paling sederhana ini. Mari kita tentukan keunggulannya:
- Keuntungan utama dari pola ini adalah kemampuan untuk melepaskan sumber daya secara pasti, yaitu saat Anda membutuhkannya.
- Keuntungan kedua adalah pengenalan cara yang sudah terbukti untuk memeriksa jika instance spesifik mengharuskan untuk menghancurkan instansnya setelah digunakan.
- Jika Anda menerapkan pola dengan benar, tipe yang dirancang akan berfungsi dengan aman dalam hal penggunaan oleh komponen pihak ketiga serta dalam hal pembongkaran dan penghancuran sumber daya saat proses macet (misalnya karena kurangnya memori). Ini adalah keuntungan terakhir.
Kekurangan
Menurut pendapat saya, pola ini memiliki lebih banyak kerugian daripada keuntungan.
- Di satu sisi, jenis apa pun yang menerapkan pola ini menginstruksikan bagian lain bahwa jika mereka menggunakannya mereka mengambil semacam penawaran publik. Ini sangat implisit bahwa karena dalam kasus penawaran publik, pengguna tipe tidak selalu tahu bahwa tipe tersebut memiliki antarmuka ini. Dengan demikian Anda harus mengikuti petunjuk IDE (ketik periode, Dis ... dan periksa apakah ada metode dalam daftar anggota kelas yang difilter). Jika Anda melihat pola Buang, Anda harus menerapkannya dalam kode Anda. Kadang-kadang itu tidak terjadi langsung dan dalam hal ini Anda harus menerapkan pola melalui sistem tipe yang menambah fungsionalitas. Contoh yang baik adalah
IEnumerator<T>
memerlukan IDisposable
. - Biasanya ketika Anda mendesain antarmuka, ada kebutuhan untuk memasukkan IDisposable ke dalam sistem antarmuka tipe ketika salah satu antarmuka harus mewarisi IDisposable. Menurut pendapat saya, ini merusak antarmuka yang kami desain. Maksud saya ketika Anda mendesain antarmuka, Anda membuat protokol interaksi terlebih dahulu. Ini adalah serangkaian tindakan yang dapat Anda lakukan dengan sesuatu yang tersembunyi di balik antarmuka.
Dispose()
adalah metode untuk menghancurkan instance kelas. Ini bertentangan dengan esensi dari protokol interaksi . Bahkan, ini adalah detail implementasi yang menyusup ke antarmuka. - Meskipun telah ditentukan, Buang () tidak berarti penghancuran objek secara langsung. Objek akan tetap ada setelah kehancurannya tetapi di negara lain. Untuk menjadikannya benar, CheckDisposed () harus menjadi perintah pertama dari setiap metode publik. Ini terlihat seperti solusi sementara yang diberikan seseorang kepada kami: "Majulah dan berlipat ganda";
- Ada juga peluang kecil untuk mendapatkan tipe yang mengimplementasikan
IDisposable
melalui implementasi eksplisit . Atau Anda bisa mendapatkan tipe yang mengimplementasikan ID yang dapat digunakan tanpa kesempatan untuk menentukan siapa yang harus menghancurkannya: Anda atau pihak yang memberikannya kepada Anda. Ini menghasilkan antipattern dari beberapa panggilan Buang () yang memungkinkan untuk menghancurkan objek yang dihancurkan; - Implementasi yang lengkap sulit, dan berbeda untuk sumber daya yang dikelola dan tidak dikelola. Di sini upaya untuk memfasilitasi pekerjaan pengembang melalui GC terlihat canggung. Anda bisa mengesampingkan metode buang
virtual void Dispose()
dan memperkenalkan beberapa jenis DisposableObject yang mengimplementasikan seluruh pola, tetapi itu tidak menyelesaikan masalah lain yang terhubung dengan pola; - Sebagai aturan, metode Buang () diimplementasikan di akhir file sementara '.tor' dideklarasikan di awal. Jika Anda memodifikasi kelas atau memperkenalkan sumber daya baru, mudah untuk lupa menambahkan pembuangan untuk mereka.
- Akhirnya, sulit untuk menentukan urutan kehancuran dalam lingkungan multithreaded ketika Anda menggunakan pola untuk grafik objek di mana objek sepenuhnya atau sebagian menerapkan pola itu. Maksud saya situasi ketika Buang () dapat mulai dari berbagai ujung grafik. Di sini lebih baik menggunakan pola lain, misalnya pola Seumur Hidup.
- Keinginan pengembang platform untuk mengotomatisasi kontrol memori yang dikombinasikan dengan kenyataan: aplikasi berinteraksi dengan kode yang tidak terkelola sangat sering + Anda perlu mengontrol pelepasan referensi ke objek sehingga Pengumpul Sampah dapat mengumpulkannya. Ini menambah kebingungan besar dalam memahami pertanyaan seperti: "Bagaimana seharusnya kita menerapkan pola dengan benar"? βApakah ada pola yang dapat diandalkan sama sekaliβ? Mungkin memanggil
delete obj; delete[] arr;
delete obj; delete[] arr;
lebih sederhana?
Bongkar domain dan keluar dari aplikasi
Jika Anda sampai di bagian ini, Anda menjadi lebih percaya diri dalam keberhasilan wawancara kerja di masa depan. Namun, kami tidak membahas semua pertanyaan yang terkait dengan pola sederhana ini, seperti yang terlihat, pola. Pertanyaan terakhir adalah apakah perilaku aplikasi berbeda dalam hal pengumpulan sampah sederhana dan ketika sampah dikumpulkan selama pembongkaran domain dan saat keluar dari aplikasi. Pertanyaan ini hanya menyentuh pada Dispose()
... Namun Dispose()
dan finalisasi berjalan seiring dan kami jarang bertemu dengan implementasi kelas yang memiliki finalisasi tetapi tidak memiliki metode Dispose()
. Jadi, mari gambarkan finalisasi di bagian terpisah. Di sini kita hanya menambahkan beberapa detail penting.
Selama pembongkaran domain aplikasi, Anda membongkar rakitan yang dimuat ke domain aplikasi dan semua objek yang dibuat sebagai bagian dari domain untuk dibongkar. Bahkan, ini berarti pembersihan (pengumpulan oleh GC) dari objek-objek ini dan memanggil finalizer untuk mereka. Jika logika finalizer menunggu finalisasi objek lain untuk dihancurkan dalam urutan yang benar, Anda dapat memperhatikan properti Environment.HasShutdownStarted
menunjukkan bahwa aplikasi diturunkan dari memori dan ke AppDomain.CurrentDomain.IsFinalizingForUnload()
metode yang menunjukkan bahwa ini domain dibongkar yang merupakan alasan untuk finalisasi. Jika peristiwa ini terjadi, urutan finalisasi sumber daya umumnya menjadi tidak penting. Kami tidak dapat menunda pembongkaran domain atau aplikasi karena kami harus melakukan semuanya secepat mungkin.
Ini adalah cara tugas ini diselesaikan sebagai bagian dari kelas LoaderAllocatorScout
// Assemblies and LoaderAllocators will be cleaned up during AppDomain shutdown in // an unmanaged code // So it is ok to skip reregistration and cleanup for finalization during appdomain shutdown. // We also avoid early finalization of LoaderAllocatorScout due to AD unload when the object was inside DelayedFinalizationList. if (!Environment.HasShutdownStarted && !AppDomain.CurrentDomain.IsFinalizingForUnload()) { // Destroy returns false if the managed LoaderAllocator is still alive. if (!Destroy(m_nativeLoaderAllocator)) { // Somebody might have been holding a reference on us via weak handle. // We will keep trying. It will be hopefully released eventually. GC.ReRegisterForFinalize(this); } }
Kesalahan implementasi yang umum
Seperti yang saya tunjukkan kepada Anda, tidak ada pola universal untuk mengimplementasikan IDisposable. Selain itu, beberapa ketergantungan pada kontrol memori otomatis menyesatkan orang dan mereka membuat keputusan yang membingungkan ketika menerapkan suatu pola. Keseluruhan .NET Framework penuh dengan kesalahan dalam implementasinya. Untuk membuktikan maksud saya, mari kita lihat kesalahan ini menggunakan contoh .NET Framework. Semua implementasi tersedia melalui: IDisposable Usages
FileEntry Class cmsinterop.cs
Kode ini ditulis terburu-buru hanya untuk menutup masalah. Jelas, penulis ingin melakukan sesuatu tetapi berubah pikiran dan menyimpan solusi yang cacat
internal class FileEntry : IDisposable { // Other fields // ... [MarshalAs(UnmanagedType.SysInt)] public IntPtr HashValue; // ... ~FileEntry() { Dispose(false); } // The implementation is hidden and complicates calling the *right* version of a method. void IDisposable.Dispose() { this.Dispose(true); } // Choosing a public method is a serious mistake that allows for incorrect destruction of // an instance of a class. Moreover, you CANNOT call this method from the outside public void Dispose(bool fDisposing) { if (HashValue != IntPtr.Zero) { Marshal.FreeCoTaskMem(HashValue); HashValue = IntPtr.Zero; } if (fDisposing) { if( MuiMapping != null) { MuiMapping.Dispose(true); MuiMapping = null; } System.GC.SuppressFinalize(this); } } }
Sistem Kelas SemaphoreSlim / Threading / SemaphoreSlim.cs
Kesalahan ini ada di bagian atas kesalahan .NET Framework mengenai IDisposable: SuppressFinalize untuk kelas di mana tidak ada finalizer. Ini sangat umum.
public void Dispose() { Dispose(true); // As the class doesn't have a finalizer, there is no need in GC.SuppressFinalize GC.SuppressFinalize(this); } // The implementation of this pattern assumes the finalizer exists. But it doesn't. // It was possible to do with just public virtual void Dispose() protected virtual void Dispose(bool disposing) { if (disposing) { if (m_waitHandle != null) { m_waitHandle.Close(); m_waitHandle = null; } m_lockObj = null; m_asyncHead = null; m_asyncTail = null; } }
Menelepon Tutup + Buang Beberapa kode proyek NativeWatcher
Terkadang orang memanggil Tutup dan Buang. Ini salah meskipun tidak akan menghasilkan kesalahan karena Buang kedua tidak menghasilkan pengecualian.
Faktanya, Close adalah pola lain untuk membuat segalanya lebih jelas bagi orang-orang. Namun, itu membuat semuanya lebih tidak jelas.
public void Dispose() { if (MainForm != null) { MainForm.Close(); MainForm.Dispose(); } MainForm = null; }
Hasil umum
- IDposable adalah standar platform dan kualitas implementasinya mempengaruhi kualitas aplikasi secara keseluruhan. Terlebih lagi, dalam beberapa situasi itu memengaruhi keamanan aplikasi Anda yang dapat diserang melalui sumber daya yang tidak dikelola.
- Implementasi IDisposable harus produktif maksimal. Ini terutama benar tentang bagian finalisasi, yang bekerja secara paralel dengan kode lainnya, memuat Pengumpul Sampah.
- Saat menerapkan IDisposable, Anda tidak boleh menggunakan Buang () bersamaan dengan metode umum kelas. Penghancuran tidak bisa seiring dengan penggunaan. Ini harus dipertimbangkan ketika mendesain tipe yang akan menggunakan objek IDisposable.
- Namun, harus ada perlindungan terhadap panggilan 'Buang ()' dari dua utas secara bersamaan. Ini hasil dari pernyataan bahwa Buang () seharusnya tidak menghasilkan kesalahan.
- Jenis yang mengandung sumber daya yang tidak dikelola harus dipisahkan dari jenis lainnya. Maksud saya jika Anda membungkus sumber daya yang tidak dikelola, Anda harus mengalokasikan jenis yang terpisah untuk itu. Tipe ini harus mengandung finalisasi dan harus diwarisi dari
SafeHandle / CriticalHandle / CriticalFinalizerObject
. Pemisahan tanggung jawab ini akan menghasilkan dukungan yang lebih baik dari sistem tipe dan akan menyederhanakan implementasi untuk menghancurkan instance tipe melalui Buang (): tipe-tipe dengan implementasi ini tidak perlu mengimplementasikan finalizer. - Secara umum, pola ini tidak nyaman digunakan serta dalam pemeliharaan kode. Mungkin, kita harus menggunakan pendekatan Inversion of Control ketika kita menghancurkan keadaan objek melalui pola
Lifetime
. Namun, kita akan membicarakannya di bagian selanjutnya.
Bab ini diterjemahkan dari bahasa Rusia bersama oleh penulis dan penerjemah profesional . Anda dapat membantu kami dengan terjemahan dari bahasa Rusia atau Inggris ke bahasa lain, terutama ke bahasa Cina atau Jerman.
Juga, jika Anda ingin berterima kasih kepada kami, cara terbaik yang dapat Anda lakukan adalah memberi kami bintang di github atau untuk repositori garpu
github / sidristij / dotnetbook .