Artikel ini adalah artikel cermin dari terjemahan mesin, silakan klik di sini untuk melompat ke artikel aslinya.

Melihat: 20809|Jawab: 1

[Sumber] Kamus Bersamaan vs. Kamus+Penguncian - Dennis Gao

[Salin tautan]
Diposting pada 13/09/2016 13.33.04 | | |
Sebelum .NET 4.0, jika kita perlu menggunakan kelas Dictionary di lingkungan multithreaded, kita tidak punya pilihan selain mengimplementasikan sinkronisasi thread sendiri untuk menjaga keamanan utas.

Banyak pengembang tentu saja telah menerapkan solusi aman utas serupa, baik dengan membuat jenis kamus aman utas yang sama sekali baru, atau hanya merangkum objek Kamus dalam kelas dan menambahkan mekanisme penguncian ke semua metode, yang kami sebut "Kamus + Kunci".

Tapi sekarang, kita memiliki ConcurrentDictionary. Deskripsi aman utas dari dokumentasi kelas Kamus di MSDN menyatakan bahwa jika Anda perlu menggunakan implementasi aman utas, gunakan ConcurrentDictionary.

Jadi, sekarang kita memiliki kelas kamus yang aman untuk utas, kita tidak perlu menerapkannya sendiri lagi. Bagus, bukan?

Asal masalah

Faktanya, saya hanya menggunakan CocurrentDictionary sekali sebelumnya, dalam pengujian saya untuk menguji responsifnya. Karena berkinerja baik dalam ujian, saya segera menggantinya dengan kelas saya, melakukan beberapa pengujian, dan kemudian terjadi kesalahan.

Jadi, apa yang salah? Bukankah Anda mengatakan benang aman?

Setelah pengujian lebih lanjut, saya menemukan akar masalahnya. Tetapi untuk beberapa alasan, MSDN versi 4.0 tidak menyertakan deskripsi tanda tangan metode GetOrAdd yang memerlukan meneruskan parameter jenis delegasi. Setelah melihat versi 4.5, saya menemukan catatan ini:

Jika Anda memanggil GetOrAdd secara bersamaan pada utas yang berbeda, addValueFactory dapat dipanggil beberapa kali, tetapi pasangan kunci/nilainya mungkin tidak ditambahkan ke kamus untuk setiap panggilan.
Itulah masalah yang saya hadapi. Karena sebelumnya tidak dijelaskan dalam dokumentasi, saya harus melakukan pengujian lebih lanjut untuk mengonfirmasi masalahnya. Tentu saja, masalah yang saya hadapi terkait dengan penggunaan saya, secara umum, saya menggunakan jenis kamus untuk menyimpan beberapa data:

Data ini sangat lambat untuk dibuat;
Data ini hanya dapat dibuat sekali, karena pembuatan kedua akan melemparkan pengecualian, atau beberapa kreasi dapat menyebabkan kebocoran sumber daya, dll.;
Saya memiliki masalah dengan kondisi kedua. Jika kedua utas menemukan bahwa sepotong data tidak ada, itu akan dibuat sekali, tetapi hanya satu hasil yang berhasil disimpan. Bagaimana dengan yang lain?

Jika proses yang Anda buat melemparkan pengecualian, Anda dapat menggunakan try: menangkap (tidak cukup elegan, tetapi itu memecahkan masalah). Tetapi bagaimana jika sumber daya dibuat dan tidak didaur ulang?

Anda mungkin mengatakan bahwa objek dibuat dan akan dikumpulkan sampah jika tidak lagi direferensikan di dalamnya. Namun, pertimbangkan apa yang akan terjadi jika situasi yang dijelaskan di bawah ini terjadi:

Hasilkan kode secara dinamis dengan Emit. Saya menggunakan pendekatan ini dalam kerangka kerja jarak jauh dan menempatkan semua implementasi dalam rakitan yang tidak dapat didaur ulang. Jika jenis dibuat dua kali, yang kedua akan selalu ada, meskipun belum pernah digunakan.
Buat utas secara langsung atau tidak langsung. Misalnya, kita perlu membangun komponen yang menggunakan utas berpemilik untuk memproses pesan asinkron dan bergantung pada urutan penerimaannya. Saat komponen dibuat, utas dibuat. Ketika instance komponen ini dihancurkan, utas juga dihentikan. Tetapi jika kita menghapus referensi ke objek setelah menghancurkan komponen, tetapi utas tidak berakhir karena alasan tertentu dan menahan referensi ke objek. Kemudian, jika benang tidak mati, benda tersebut juga tidak akan didaur ulang.
Lakukan operasi P/Invoke. Mengharuskan jumlah waktu penutupan untuk pegangan yang diterima harus sama dengan jumlah bukaan.
Yang pasti, ada banyak situasi serupa. Misalnya, objek kamus akan menahan koneksi ke layanan di server jarak jauh, yang hanya dapat diminta sekali, dan jika diminta untuk kedua kalinya, layanan lain akan berpikir bahwa beberapa jenis kesalahan telah terjadi dan mencatatnya di log. (Di perusahaan tempat saya bekerja, ada beberapa hukuman hukum untuk kondisi ini.) )
Jadi, mudah untuk melihat bahwa Dictionary + Locks tidak dapat diganti dengan ConcurrentDictionary, bahkan jika dokumentasi mengatakan itu aman untuk utas.

Analisis masalahnya

Masih belum mengerti?

Memang benar bahwa masalah ini mungkin tidak muncul di bawah pendekatan Kamus + Kunci. Karena ini tergantung pada implementasi tertentu, mari kita lihat contoh sederhana ini:


Dalam kode di atas, kita menahan kunci pada kamus sebelum mulai mengkueri nilai kunci. Jika pasangan kunci-nilai yang ditentukan tidak ada, itu akan dibuat secara langsung. Pada saat yang sama, karena kita sudah memegang kunci pada kamus itu, kita dapat menambahkan pasangan kunci-nilai langsung ke kamus. Kemudian lepaskan kunci kamus, dan kembalikan hasilnya. Jika dua utas mengkueri nilai kunci yang sama pada saat yang sama, utas pertama yang mendapatkan kunci kamus akan menyelesaikan pembuatan objek, dan utas lainnya akan menunggu penyelesaian pembuatan ini dan mendapatkan hasil nilai kunci yang dibuat setelah mendapatkan kunci kamus.

Itu bagus, bukan?

Benar-benar tidak! Saya tidak berpikir membuat objek secara paralel seperti ini, di mana hanya satu yang digunakan pada akhirnya, tidak menciptakan masalah yang telah saya jelaskan.

Situasi dan masalah yang saya coba uraikan mungkin tidak selalu dapat direproduksi, dalam lingkungan paralel kita cukup membuat dua objek dan kemudian membuang satu. Jadi, bagaimana tepatnya kita membandingkan Dictionary + Locks dan ConcurrentDictionary?

Jawabannya adalah: itu tergantung pada strategi penggunaan kunci dan bagaimana kamus digunakan.

Game 1: Buat objek yang sama secara paralel

Pertama, mari kita asumsikan bahwa suatu objek dapat dibuat dua kali, jadi apa yang terjadi jika dua utas membuat objek ini secara bersamaan?

Kedua, berapa lama kita menghabiskan waktu untuk kreasi serupa?

Kita cukup membuat contoh di mana pembuatan instance objek membutuhkan waktu 10 detik. Ketika utas pertama membuat objek 5 detik kemudian, implementasi kedua mencoba memanggil metode GetOrAdd untuk mendapatkan objek, dan karena objek masih belum ada, objek juga mulai membuat objek.

Dalam kondisi ini, kita memiliki 2 CPU yang bekerja secara paralel selama 5 detik, dan ketika thread pertama selesai bekerja, thread kedua masih perlu terus berjalan selama 5 detik untuk menyelesaikan konstruksi objek. Ketika utas kedua selesai membangun objek, ia menemukan bahwa objek sudah ada, dan memilih untuk menggunakan objek yang ada dan membuang objek yang baru dibuat secara langsung.

Jika utas kedua hanya menunggu dan CPU kedua melakukan beberapa pekerjaan lain (menjalankan utas atau aplikasi lain, menghemat daya), itu akan mendapatkan objek yang diinginkan setelah 5 detik, bukan 10 detik.

Jadi, dalam kondisi ini, Dictionary + Locks memenangkan permainan kecil.

Game 2: Kunjungi objek yang berbeda secara paralel

Tidak, situasi yang Anda katakan tidak benar sama sekali!

Nah, contoh di atas agak aneh, tetapi memang menggambarkan masalahnya, hanya saja penggunaan ini lebih ekstrem. Jadi, pertimbangkan apa yang terjadi jika utas pertama membuat objek, dan utas kedua perlu mengakses objek nilai-kunci lain, dan objek nilai-kunci itu sudah ada?

Di ConcurrentDictionary, desain bebas kunci membuat pembacaan sangat cepat karena tidak ada kunci pada pembacaan. Dalam kasus Dictionary + Locks, operasi baca akan dikunci secara saling eksklusif, bahkan jika itu adalah tombol yang sama sekali berbeda, yang jelas akan memperlambat operasi membaca.

Dengan cara ini, ConcurrentDictionary menarik kembali permainan.

Catatan: Di sini saya menganggap bahwa Anda memahami beberapa konsep seperti Bucket/Node/Entry di kelas kamus, jika tidak, disarankan untuk membaca artikel Ofir Makmal "Memahami Kamus Generik secara mendalam", yang menjelaskan konsep-konsep ini dengan baik.

Game ketiga dari game ini: baca lebih lanjut dan tulis single

Apa yang terjadi jika Anda menggunakan Beberapa Pembaca dan Penulis Tunggal alih-alih kunci penuh pada kamus di Kamus + Kunci?

Jika utas membuat objek dan menahan kunci yang dapat ditingkatkan hingga objek dibuat, kunci ditingkatkan ke kunci tulis, maka operasi baca dapat dilakukan secara paralel.

Kita juga dapat memecahkan masalah dengan membiarkan operasi baca menganggur selama 10 detik. Tetapi jika ada jauh lebih banyak pembacaan daripada tulisan, kita akan menemukan bahwa ConcurrentDictionary masih cepat karena mengimplementasikan pembacaan mode bebas kunci.

Menggunakan ReaderWriterLockSlim untuk Kamus membuat pembacaan lebih buruk, dan umumnya disarankan untuk menggunakan Full Lock untuk Kamus alih-alih ReaderWriterLockSlim.

Jadi, dalam kondisi ini, ConcurrentDictionary memenangkan game lain.

Catatan: Saya telah membahas kelas YieldReaderWriterLock dan YieldReaderWriterLockSlim di artikel sebelumnya. Dengan menggunakan kunci baca-tulis ini, kecepatannya telah ditingkatkan secara signifikan (sekarang berkembang menjadi SpinReaderWriterLockSlim) dan memungkinkan beberapa pembacaan dijalankan secara paralel dengan sedikit atau tanpa dampak. Meskipun saya masih menggunakan cara ini, ConcurrentDictionary tanpa kunci jelas akan lebih cepat.

Game 4: Tambahkan beberapa pasangan kunci-nilai

Pertarungan belum berakhir.

Bagaimana jika kita memiliki beberapa nilai kunci untuk ditambahkan, dan semuanya tidak bertabrakan dan ditetapkan dalam bucket yang berbeda?

Awalnya, pertanyaan ini aneh, tetapi saya melakukan tes yang tidak cocok. Saya menggunakan kamus tipe <int, int> dan pabrik konstruksi objek akan mengembalikan hasil negatif secara langsung sebagai kunci.

Saya mengharapkan ConcurrentDictionary menjadi yang tercepat, tetapi ternyata yang paling lambat. Kamus + Kunci, di sisi lain, bekerja lebih cepat. Mengapa demikian?

Ini karena ConcurrentDictionary mengalokasikan simpul dan menempatkannya di bucket yang berbeda, yang dioptimalkan untuk memenuhi desain bebas kunci untuk operasi baca. Namun, saat menambahkan item nilai kunci, proses pembuatan node menjadi mahal.

Bahkan dalam kondisi paralel, mengalokasikan kunci Node masih memakan lebih banyak waktu daripada menggunakan kunci penuh.

Jadi, Dictionary + Locks memenangkan game ini.

Memainkan game kelima: Frekuensi operasi membaca lebih tinggi

Terus terang, jika kita memiliki delegasi yang dapat dengan cepat membuat instance objek, kita tidak memerlukan Kamus. Kita bisa langsung memanggil delegasi untuk mendapatkan objeknya, bukan?

Padahal, jawabannya juga tergantung pada situasinya.

Bayangkan bahwa jenis kunci adalah string dan berisi peta jalur untuk berbagai halaman di server web, dan nilai yang sesuai adalah jenis objek yang berisi catatan pengguna saat ini yang mengakses halaman dan jumlah semua kunjungan ke halaman sejak server dimulai.

Membuat objek seperti ini hampir seketika. Dan setelah itu, Anda tidak perlu membuat objek baru, cukup ubah nilai yang disimpan di dalamnya. Jadi dimungkinkan untuk mengizinkan pembuatan cara dua kali sampai hanya satu instance yang digunakan. Namun, karena ConcurrentDictionary mengalokasikan sumber daya Node lebih lambat, menggunakan Dictionary + Locks akan menghasilkan waktu pembuatan yang lebih cepat.

Jadi, dengan contoh ini sangat istimewa, kita juga melihat bahwa Dictionary + Locks berkinerja lebih baik dalam kondisi ini, memakan waktu lebih sedikit.

Meskipun alokasi node di ConcurrentDictionary lebih lambat, saya tidak mencoba memasukkan 100 juta item data ke dalamnya untuk menguji waktu. Karena itu jelas membutuhkan banyak waktu.

Tetapi dalam kebanyakan kasus, setelah item data dibuat, item tersebut selalu dibaca. Bagaimana konten item data berubah adalah masalah lain. Jadi tidak masalah berapa milidetik lagi yang dibutuhkan untuk membuat item data, karena pembacaan lebih cepat (hanya beberapa milidetik lebih cepat), tetapi pembacaan terjadi lebih sering.

Jadi, ConcurrentDictionary memenangkan permainan.

Game 6: Buat objek yang menghabiskan waktu yang berbeda

Apa yang terjadi jika waktu yang diperlukan untuk membuat item data yang berbeda bervariasi?

Buat beberapa item data yang menghabiskan waktu yang berbeda dan tambahkan ke kamus secara paralel. Ini adalah poin terkuat dari ConcurrentDictionary.

ConcurrentDictionary menggunakan sejumlah mekanisme penguncian yang berbeda untuk memungkinkan item data ditambahkan secara bersamaan, tetapi logika seperti memutuskan kunci mana yang akan digunakan, meminta kunci untuk mengubah ukuran bucket, dll., tidak membantu. Kecepatan di mana item data dimasukkan ke dalam ember sangat cepat dari mesin. Apa yang benar-benar membuat ConcurrentDictionary menang adalah kemampuannya untuk membuat objek secara paralel.

Namun, kita sebenarnya bisa melakukan hal yang sama. Jika kita tidak peduli apakah kita membuat objek secara paralel, atau jika beberapa di antaranya telah dibuang, kita dapat menambahkan kunci untuk mendeteksi apakah item data sudah ada, lalu lepaskan kunci, buat item data, tekan untuk mendapatkan kunci, periksa lagi apakah item data ada, dan jika tidak, tambahkan item data. Kodenya mungkin terlihat seperti ini:

* Perhatikan bahwa saya menggunakan kamus tipe <int, int>.

Dalam struktur sederhana di atas, Dictionary + Locks berkinerja hampir sama baiknya dengan ConcurrentDictionary saat membuat dan menambahkan item data dalam kondisi paralel. Tetapi ada juga masalah yang sama, di mana beberapa nilai dapat dihasilkan tetapi tidak pernah digunakan.

kesimpulan

Jadi, apakah ada kesimpulan?

Saat ini, masih ada beberapa:

Semua kelas kamus sangat cepat. Meskipun saya telah membuat jutaan data, itu masih cepat. Biasanya, kita hanya membuat sejumlah kecil item data, dan ada beberapa interval waktu antara pembacaan, jadi kita umumnya tidak memperhatikan overhead waktu membaca item data.
Jika objek yang sama tidak dapat dibuat dua kali, jangan gunakan ConcurrentDictionary.
Jika Anda benar-benar khawatir tentang kinerja, Dictionary + Locks mungkin masih merupakan solusi yang baik. Faktor penting adalah jumlah item data yang ditambahkan dan dihapus. Tetapi jika ada banyak operasi baca, itu lebih lambat daripada ConcurrentDictionary.
Meskipun saya tidak memperkenalkannya, sebenarnya ada lebih banyak kebebasan untuk menggunakan skema Kamus + Kunci. Misalnya, Anda dapat mengunci sekali, menambahkan beberapa item data, menghapus beberapa item data, atau mengkueri beberapa kali, dll., lalu melepaskan kunci.
Secara umum, hindari menggunakan ReaderWriterLockSlim jika ada lebih banyak bacaan daripada tulisan. Jenis kamus sudah jauh lebih cepat daripada mendapatkan kunci baca dalam kunci baca-tulis. Tentu saja, ini juga tergantung pada waktu yang dihabiskan untuk membuat objek dalam kunci.
Jadi, saya pikir contoh yang diberikan agak ekstrem, tetapi mereka menunjukkan bahwa menggunakan ConcurrentDictionary tidak selalu merupakan solusi terbaik.

Rasakan perbedaannya

Saya menulis artikel ini dengan maksud mencari solusi yang lebih baik.

Saya sudah mencoba untuk mendapatkan pemahaman yang lebih dalam tentang cara kerja kelas kamus tertentu (sekarang saya merasa sangat jelas).

Bisa dibilang, Bucket dan Node di ConcurrentDictionary sangat sederhana. Saya melakukan hal serupa ketika saya mencoba membuat kelas kamus. Kelas Kamus biasa mungkin tampak lebih sederhana, tetapi pada kenyataannya, itu lebih kompleks.

Di ConcurrentDictionary, setiap Node adalah kelas lengkap. Di kelas Kamus, Node diimplementasikan menggunakan jenis nilai, dan semua simpul disimpan dalam array besar, sedangkan Bucket digunakan untuk mengindeks dalam array. Ini juga digunakan sebagai pengganti referensi sederhana Node ke Node berikutnya (bagaimanapun, sebagai Node dari tipe struct, itu tidak dapat berisi anggota Node dari tipe struct).

Saat menambahkan dan menghapus kamus, kelas Dictionary tidak dapat begitu saja membuat node baru, kelas harus memeriksa apakah ada indeks yang menandai node yang telah dihapus, dan kemudian menggunakannya kembali. Atau "Count" digunakan untuk mendapatkan posisi Node baru dalam array. Faktanya, ketika array penuh, kelas Kamus memaksa perubahan ukuran.

Untuk ConcurrentDictionary, Node dapat dianggap sebagai objek baru. Menghapus Node hanyalah menghapus referensinya. Menambahkan Node baru dapat dengan mudah membuat instance Node baru. Mengubah ukuran hanya untuk menghindari konflik, tetapi tidak wajib.

Jadi, jika kelas Dictionary sengaja menggunakan algoritme yang lebih kompleks untuk menanganinya, bagaimana ConcurrentDictionary akan memastikan bahwa kinerjanya lebih baik di lingkungan multi-threaded?

Yang benar adalah: menempatkan semua node dalam satu array adalah cara tercepat untuk mengalokasikan dan membaca, bahkan jika kita memerlukan array lain untuk melacak di mana menemukan item data tersebut. Jadi sepertinya memiliki jumlah bucket yang sama akan menggunakan lebih banyak memori, tetapi item data baru tidak perlu dialokasikan ulang, tidak diperlukan sinkronisasi objek baru, dan pengumpulan sampah baru tidak terjadi. Karena semuanya sudah ada.

Namun, mengganti konten dalam Node bukanlah operasi atom, yang merupakan salah satu faktor yang membuat utasnya tidak aman. Karena simpul adalah semua objek, simpul awalnya dibuat, dan kemudian referensi terpisah diperbarui untuk menunjuk ke sana (operasi atom di sini). Jadi, utas yang dibaca dapat membaca konten kamus tanpa kunci, dan bacaan harus menjadi salah satu nilai lama dan baru, dan tidak ada kemungkinan membaca nilai yang tidak lengkap.

Jadi, kenyataannya adalah: jika Anda tidak memerlukan kunci, kelas Kamus lebih cepat dalam membaca, karena kuncilah yang memperlambat pembacaan.

Artikel ini diterjemahkan dari artikel Paulo Zemek "Dictionary + Locking versus ConcurrentDictionary" di CodeProject, dan beberapa pernyataan akan berubah karena alasan pemahaman.







Mantan:Autofac yang efisien IoC
Depan:Alibaba 4 orang dipecat karena menggunakan skrip JS untuk terburu-buru membeli kue bulan
 Tuan tanah| Diposting pada 13/09/2016 13.33.15 |
ConcurrentDictionary mendukung pembaruan baru dan yang diperbarui
http://www.itsvse.com/thread-2955-1-1.html
(Sumber: Jaringan Pertanian Kode)
Sanggahan:
Semua perangkat lunak, materi pemrograman, atau artikel yang diterbitkan oleh Code Farmer Network hanya untuk tujuan pembelajaran dan penelitian; Konten di atas tidak boleh digunakan untuk tujuan komersial atau ilegal, jika tidak, pengguna akan menanggung semua konsekuensi. Informasi di situs ini berasal dari Internet, dan sengketa hak cipta tidak ada hubungannya dengan situs ini. Anda harus sepenuhnya menghapus konten di atas dari komputer Anda dalam waktu 24 jam setelah pengunduhan. Jika Anda menyukai program ini, harap dukung perangkat lunak asli, pembelian pendaftaran, dan dapatkan layanan asli yang lebih baik. Jika ada pelanggaran, silakan hubungi kami melalui email.

Mail To:help@itsvse.com