.NET - Kamus penguncian vs. ConcurrentDictionary


131

Saya tidak dapat menemukan informasi yang cukup tentang ConcurrentDictionaryjenis, jadi saya pikir saya akan menanyakannya di sini.

Saat ini, saya menggunakan a Dictionaryuntuk menahan semua pengguna yang diakses secara konstan oleh beberapa utas (dari kumpulan utas, jadi tidak ada jumlah utas yang tepat), dan itu memiliki akses yang disinkronkan.

Saya baru-baru ini menemukan bahwa ada satu set koleksi thread-safe di .NET 4.0, dan tampaknya sangat menyenangkan. Saya bertanya-tanya, apa yang akan menjadi opsi 'lebih efisien dan lebih mudah dikelola', karena saya memiliki pilihan antara memiliki normal Dictionarydengan akses yang disinkronkan, atau memiliki ConcurrentDictionaryyang sudah aman-thread.

Referensi untuk .NET 4.0 ConcurrentDictionary


3
Anda mungkin ingin melihat artikel proyek kode ini pada subjek
nawfal

Jawaban:


146

Koleksi thread-safe vs. koleksi non-threadsafe dapat dilihat dengan cara yang berbeda.

Pertimbangkan toko tanpa petugas, kecuali saat checkout. Anda memiliki banyak masalah jika orang tidak bertindak secara bertanggung jawab. Sebagai contoh, katakanlah seorang pelanggan mengambil kaleng dari piramida-saat petugas sedang membangun piramida, neraka akan pecah. Atau, bagaimana jika dua pelanggan meraih barang yang sama secara bersamaan, siapa yang menang? Apakah akan ada perkelahian? Ini adalah koleksi non-threadsafe. Ada banyak cara untuk menghindari masalah, tetapi mereka semua memerlukan semacam penguncian, atau lebih tepatnya akses eksplisit dalam beberapa cara.

Di sisi lain, pertimbangkan toko dengan pegawai di meja, dan Anda hanya bisa berbelanja melaluinya. Anda mendapatkan antrean, dan meminta item kepadanya, dia membawanya kembali kepada Anda, dan Anda keluar dari barisan. Jika Anda membutuhkan lebih dari satu item, Anda hanya dapat mengambil sebanyak mungkin item pada setiap perjalanan pulang pergi yang Anda ingat, tetapi Anda harus berhati-hati untuk menghindari memonopoli petugas, ini akan membuat pelanggan lain marah di belakang Anda.

Sekarang pertimbangkan ini. Di toko dengan satu petugas, bagaimana jika Anda sampai di garis depan, dan tanyakan kepada petugas "Apakah Anda punya kertas toilet", dan dia berkata "Ya", dan kemudian Anda pergi "Oke, saya Aku akan kembali kepadamu ketika aku tahu berapa yang kubutuhkan ", maka pada saat kau kembali di garis depan, toko tentu saja bisa terjual habis. Skenario ini tidak dicegah oleh koleksi threadsafe.

Kumpulan threadsafe menjamin bahwa struktur data internal valid setiap saat, bahkan jika diakses dari banyak utas.

Koleksi non-threadsafe tidak disertai dengan jaminan semacam itu. Misalnya, jika Anda menambahkan sesuatu ke pohon biner pada satu utas, sedangkan utas lainnya sibuk menyeimbangkan kembali pohon tersebut, tidak ada jaminan item akan ditambahkan, atau bahkan pohon itu masih valid setelahnya, mungkin korup di luar harapan.

Namun, kumpulan threadsafe tidak menjamin bahwa operasi berurutan pada utas semuanya bekerja pada "snapshot" yang sama dari struktur data internal, yang berarti bahwa jika Anda memiliki kode seperti ini:

if (tree.Count > 0)
    Debug.WriteLine(tree.First().ToString());

Anda mungkin mendapatkan NullReferenceException karena inbetween tree.Countdan tree.First(), utas lain telah membersihkan node yang tersisa di pohon, yang berarti First()akan kembali null.

Untuk skenario ini, Anda perlu melihat apakah koleksi tersebut memiliki cara yang aman untuk mendapatkan apa yang Anda inginkan, mungkin Anda perlu menulis ulang kode di atas, atau Anda mungkin perlu mengunci.


9
Setiap kali saya membaca artikel ini, meskipun itu semua benar, bahwa bagaimanapun kita salah jalan.
scope_creep

19
Masalah utama dalam aplikasi yang diaktifkan thread adalah struktur data yang bisa berubah. Jika Anda bisa menghindari itu, Anda akan menyelamatkan diri dari banyak masalah.
Lasse V. Karlsen

89
Ini adalah penjelasan yang bagus tentang keamanan utas, namun saya tidak bisa tidak merasa bahwa ini sebenarnya tidak menjawab pertanyaan OP. Apa yang ditanyakan OP (dan apa yang kemudian saya temui dalam mencari pertanyaan ini) adalah perbedaan antara menggunakan standar Dictionarydan menangani penguncian diri sendiri vs menggunakan ConcurrentDictionarytipe yang dibangun pada .NET 4+. Saya sebenarnya sedikit bingung bahwa ini diterima.
mclark1129

4
Thread-safety lebih dari sekadar menggunakan koleksi yang tepat. Menggunakan koleksi yang tepat adalah awal, bukan satu-satunya hal yang harus Anda tangani. Saya kira itulah yang ingin diketahui OP. Saya tidak bisa menebak mengapa ini diterima, sudah lama sejak saya menulis ini.
Lasse V. Karlsen

2
Saya pikir apa yang coba dikatakan oleh @ LasseV.Karlsen, adalah bahwa ... keamanan thread mudah untuk dicapai, tetapi Anda harus menyediakan tingkat konkurensi dalam bagaimana Anda menangani keamanan itu. Tanyakan pada diri Anda sendiri, "Bisakah saya melakukan lebih banyak operasi pada saat yang bersamaan sambil menjaga objek tetap aman?" Pertimbangkan contoh ini: Dua pelanggan ingin mengembalikan barang yang berbeda. Ketika Anda, sebagai manajer, melakukan perjalanan untuk mengisi kembali toko Anda, tidak bisakah Anda mengisi kembali kedua barang dalam satu perjalanan dan masih menangani permintaan kedua pelanggan, atau apakah Anda akan melakukan perjalanan setiap kali pelanggan mengembalikan barang?
Alexandru

68

Anda masih harus sangat berhati-hati saat menggunakan koleksi thread-safe karena thread-safe tidak berarti Anda dapat mengabaikan semua masalah threading. Ketika suatu koleksi mengiklankan dirinya sebagai aman-utas, biasanya itu berarti tetap dalam keadaan konsisten bahkan ketika beberapa utas membaca dan menulis secara bersamaan. Tetapi itu tidak berarti bahwa utas tunggal akan melihat urutan hasil "logis" jika ia memanggil beberapa metode.

Misalnya, jika Anda pertama kali memeriksa apakah kunci ada dan kemudian mendapatkan nilai yang sesuai dengan kunci tersebut, kunci itu mungkin tidak lagi ada bahkan dengan versi ConcurrentDictionary (karena utas lain mungkin telah menghapus kunci). Anda masih perlu menggunakan penguncian dalam kasus ini (atau lebih baik: menggabungkan dua panggilan dengan menggunakan TryGetValue ).

Jadi jangan menggunakannya, tetapi jangan berpikir bahwa itu memberi Anda izin gratis untuk mengabaikan semua masalah konkurensi. Anda masih harus berhati-hati.


4
Jadi pada dasarnya Anda katakan jika Anda tidak menggunakan objek dengan benar itu tidak akan berhasil?
ChaosPandion

3
Pada dasarnya Anda perlu menggunakan TryAdd, bukan yang umum Berisi -> Tambah.
ChaosPandion

3
@ Bruno: Tidak. Jika Anda belum mengunci, utas lainnya mungkin telah menghapus kunci antara dua panggilan.
Mark Byers

13
Jangan bermaksud terdengar singkat di sini, tetapi TryGetValueselalu menjadi bagian dari Dictionarydan selalu menjadi pendekatan yang direkomendasikan. Metode "bersamaan" penting yang ConcurrentDictionarymemperkenalkan adalah AddOrUpdatedan GetOrAdd. Jadi, jawaban yang bagus, tetapi bisa saja mengambil contoh yang lebih baik.
Aaronaught

4
Benar - tidak seperti dalam database yang memiliki transaksi, struktur pencarian dalam memori yang paling bersamaan kehilangan konsep isolasi , mereka hanya menjamin struktur data internal benar.
Chang

39

Internal ConcurrentDictionary menggunakan kunci terpisah untuk setiap ember hash. Selama Anda hanya menggunakan Tambah / TryGetValue dan metode sejenisnya yang berfungsi pada satu entri, kamus akan berfungsi sebagai struktur data yang hampir bebas kunci dengan masing-masing manfaat kinerja yang manis. OTOH metode enumerasi (termasuk properti Count) mengunci semua ember sekaligus dan karenanya lebih buruk daripada Kamus yang disinkronkan, berdasarkan kinerja.

Saya katakan, cukup gunakan ConcurrentDictionary.


15

Saya pikir metode ConcurrentDictionary.GetOrAdd adalah persis apa yang paling dibutuhkan skenario multi-threaded.


1
Saya akan menggunakan TryGet juga, jika tidak Anda membuang-buang upaya dalam menginisialisasi parameter kedua untuk GetOrAdd setiap kali kunci sudah ada.
Jorrit Schippers

7
GetOrAdd memiliki kelebihan GetOrAdd (TKey, Func <TKey, TValue>) , sehingga parameter kedua dapat diinisialisasi dengan malas hanya jika kunci tidak ada dalam kamus. Selain itu TryGetValue digunakan untuk mengambil data, bukan memodifikasi. Panggilan selanjutnya ke masing-masing metode ini dapat menyebabkan utas lainnya mungkin telah menambahkan atau menghapus kunci antara dua panggilan.
sgnsajgon

Ini harus menjadi jawaban yang ditandai untuk pertanyaan itu, meskipun jabatan Lasse sangat bagus.
Spivonious

13

Sudahkah Anda melihat Ekstensi Reaktif untuk. Net 3.5sp1. Menurut Jon Skeet, mereka telah melakukan backport bundel ekstensi paralel dan struktur data bersamaan untuk .Net3.5 sp1.

Ada satu set sampel untuk. Net 4 Beta 2, yang menjelaskan secara cukup baik tentang cara menggunakannya ekstensi paralel.

Saya baru saja menghabiskan minggu lalu menguji ConcurrentDictionary menggunakan 32 utas untuk melakukan I / O. Tampaknya berfungsi seperti yang diiklankan, yang akan menunjukkan sejumlah besar pengujian telah dimasukkan ke dalamnya.

Edit : .NET 4 ConcurrentDictionary dan pola.

Microsoft telah merilis pdf yang disebut Pola Pemrograman Paralell. Benar-benar layak untuk diunduh karena dijelaskan dalam detail yang sangat bagus pola yang tepat untuk digunakan untuk .Net 4 Ekstensi bersamaan dan pola anti yang harus dihindari. Ini dia.


4

Pada dasarnya Anda ingin menggunakan ConcurrentDictionary baru. Tepat di luar kotak Anda harus menulis lebih sedikit kode untuk membuat program yang aman.


Apakah ada masalah jika saya melanjutkan dengan Kamus Serentak?
kbvishnu

1

Ini ConcurrentDictionaryadalah pilihan yang bagus jika memenuhi semua kebutuhan keselamatan benang Anda. Jika tidak, dengan kata lain Anda melakukan sesuatu yang sedikit rumit, Dictionary+ yang normal lockmungkin merupakan pilihan yang lebih baik. Misalnya, katakanlah Anda menambahkan beberapa pesanan ke dalam kamus, dan Anda ingin terus memperbarui jumlah total pesanan. Anda dapat menulis kode seperti ini:

private ConcurrentDictionary<int, Order> _dict;
private Decimal _totalAmount = 0;

while (await enumerator.MoveNextAsync())
{
    Order order = enumerator.Current;
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

Kode ini tidak aman untuk thread. Beberapa utas yang memperbarui _totalAmountbidang dapat menyebabkannya dalam kondisi rusak. Jadi, Anda dapat mencoba melindunginya dengan lock:

_dict.TryAdd(order.Id, order);
lock (_locker) _totalAmount += order.Amount;

Kode ini "lebih aman", tetapi masih belum aman. Tidak ada jaminan bahwa _totalAmountkonsisten dengan entri dalam kamus. Utas lain mungkin mencoba membaca nilai-nilai ini, untuk memperbarui elemen UI:

Decimal totalAmount;
lock (_locker) totalAmount = _totalAmount;
UpdateUI(_dict.Count, totalAmount);

The totalAmountmungkin tidak sesuai dengan hitungan perintah dalam kamus. Statistik yang ditampilkan mungkin salah. Pada titik ini Anda akan menyadari bahwa Anda harus memperluas lockperlindungan untuk memasukkan pembaruan kamus:

// Thread A
lock (_locker)
{
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

// Thread B
int ordersCount;
Decimal totalAmount;
lock (_locker)
{
    ordersCount = _dict.Count;
    totalAmount = _totalAmount;
}
UpdateUI(ordersCount, totalAmount);

Kode ini sangat aman, tetapi semua manfaat menggunakan a ConcurrentDictionaryhilang.

  1. Performa menjadi lebih buruk daripada menggunakan normal Dictionary, karena penguncian internal di dalam ConcurrentDictionarysekarang boros dan berlebihan.
  2. Anda harus meninjau semua kode Anda untuk penggunaan variabel-variabel bersama yang tidak dilindungi.
  3. Anda terjebak dengan menggunakan API yang canggung ( TryAdd?, AddOrUpdate?).

Jadi saran saya adalah: mulailah dengan tanda Dictionary+ lock, dan simpan opsi untuk meningkatkan versi nanti ConcurrentDictionarysebagai optimasi kinerja, jika opsi ini benar-benar layak. Dalam banyak kasus itu tidak akan terjadi.


0

Kami telah menggunakan ConcurrentDictionary untuk koleksi cache, yang diisi ulang setiap 1 jam dan kemudian dibaca oleh banyak utas klien, mirip dengan solusi untuk Apakah ini contoh utas aman? pertanyaan.

Kami menemukan, bahwa mengubahnya menjadi  ReadOnlyDictionary  meningkatkan kinerja secara keseluruhan.


ReadOnlyDictionary hanyalah pembungkus IDictionary lain. Apa yang Anda gunakan di bawah tenda?
Lu55

@ Lu55, kami menggunakan perpustakaan MS .Net dan memilih di antara kamus yang tersedia / berlaku. Saya tidak mengetahui tentang implementasi internal MS dari ReadOnlyDictionary
Michael Freidgeim
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.