Saya sangat senang melihat System.Collections.Concurrent
namespace baru di. Net 4.0, cukup bagus! Aku pernah melihat ConcurrentDictionary
, ConcurrentQueue
, ConcurrentStack
, ConcurrentBag
dan BlockingCollection
.
Satu hal yang tampaknya hilang secara misterius adalah a ConcurrentList<T>
. Apakah saya harus menulis sendiri (atau mengeluarkannya dari web :))?
Apakah saya melewatkan sesuatu yang jelas di sini?
Jawaban:
Saya mencobanya beberapa waktu lalu (juga: di GitHub ). Implementasi saya memiliki beberapa masalah, yang tidak akan saya bahas di sini. Biarkan saya memberi tahu Anda, yang lebih penting, apa yang saya pelajari.
Pertama, tidak mungkin Anda akan mendapatkan implementasi penuh
IList<T>
yang tidak terkunci dan aman. Secara khusus, penyisipan dan pemindahan acak tidak akan berfungsi, kecuali jika Anda juga melupakan O (1) akses acak (yaitu, kecuali jika Anda "menipu" dan hanya menggunakan semacam daftar yang ditautkan dan membiarkan pengindeksan menyedot).Apa yang saya pikir mungkin ada baiknya adalah, bagian terbatas benang-aman dari
IList<T>
: khususnya, yang akan memungkinkanAdd
dan menyediakan random read-only akses dengan indeks (tapi tidak adaInsert
,RemoveAt
, dll, dan juga tidak ada random write akses).Ini adalah tujuan implementasi saya
ConcurrentList<T>
. Tetapi ketika saya menguji kinerjanya dalam skenario multithreaded, saya menemukan bahwa hanya menyinkronkan menambahkan keList<T>
lebih cepat . Pada dasarnya, menambahkan keList<T>
sudah cepat kilat; kompleksitas langkah-langkah komputasi yang terlibat adalah sangat kecil (menambah indeks dan menetapkan ke elemen dalam array; itu benar - benar itu ). Anda akan membutuhkan satu ton tulisan bersamaan untuk melihat segala jenis pertikaian kunci tentang ini; dan bahkan kemudian, kinerja rata-rata dari setiap penulisan masih akan mengalahkan yang lebih mahal meskipun implementasi tanpa kunci diConcurrentList<T>
.Dalam peristiwa yang relatif jarang bahwa array internal daftar perlu mengubah ukuran sendiri, Anda membayar biaya yang kecil. Jadi pada akhirnya saya menyimpulkan bahwa ini adalah satu ceruk skenario di mana
ConcurrentList<T>
jenis koleksi add-only akan masuk akal: ketika Anda ingin overhead rendah dijamin menambahkan elemen pada setiap panggilan (sehingga, sebagai lawan dari tujuan kinerja diamortisasi).Ini hanya kelas yang hampir tidak berguna seperti yang Anda pikirkan.
sumber
List<T>
yang menggunakan sinkronisasi skool-tua berbasis monitor, ada yangSynchronizedCollection<T>
tersembunyi di BCL: msdn.microsoft.com/en-us/library/ms668265.aspxConcurrentList
akan menjadi kemenangan adalah ketika tidak ada banyak aktivitas yang ditambahkan ke daftar, tetapi ada banyak pembaca secara bersamaan. Orang bisa mengurangi overhead pembaca menjadi penghalang memori tunggal (dan menghilangkan bahkan jika pembaca tidak khawatir tentang data yang sedikit basi).ConcurrentList<T>
sedemikian rupa sehingga pembaca dijamin melihat kondisi yang konsisten tanpa perlu mengunci apa pun, dengan overhead yang relatif sedikit ditambahkan. Ketika daftar mengembang dari misalnya ukuran 32 hingga 64, pertahankan ukuran-32 array dan buat ukuran-64 array baru. Saat menambahkan masing-masing 32 item berikutnya, letakkan di slot 32-63 dari array baru dan salin item lama dari array ukuran-32 ke yang baru. Sampai item ke-64 ditambahkan, pembaca akan mencari di array ukuran-32 untuk item 0-31 dan di array ukuran-64 untuk item 32-63.Untuk apa Anda menggunakan ConcurrentList?
Konsep wadah Akses Acak di dunia berulir tidak berguna seperti yang terlihat. Pernyataan
secara keseluruhan masih belum aman.
Alih-alih membuat ConcurrentList, cobalah untuk membangun solusi dengan apa yang ada di sana. Kelas yang paling umum adalah ConcurrentBag dan terutama BlockingCollection.
sumber
Monitor
untuk melakukan itu, tidak ada alasan untuk daftar bersamaan.Dengan segala hormat pada jawaban-jawaban luar biasa yang telah diberikan, ada saatnya saya hanya menginginkan IList yang aman utas. Tidak ada yang maju atau mewah. Kinerja penting dalam banyak kasus tetapi kadang-kadang itu bukan masalah. Ya, akan selalu ada tantangan tanpa metode seperti "TryGetValue" dll, tetapi kebanyakan kasus saya hanya ingin sesuatu yang dapat saya sebutkan tanpa perlu khawatir tentang meletakkan kunci di sekitar segalanya. Dan ya, seseorang mungkin dapat menemukan beberapa "bug" dalam implementasi saya yang mungkin menyebabkan kebuntuan atau sesuatu (saya kira) tetapi mari kita jujur: Ketika datang ke multi-threading, jika Anda tidak menulis kode Anda dengan benar, itu akan menemui jalan buntu. Dengan pemikiran itu saya memutuskan untuk membuat implementasi ConcurrentList sederhana yang menyediakan kebutuhan dasar ini.
Dan untuk apa nilainya: Saya melakukan tes dasar untuk menambahkan 10.000.000 item ke Daftar dan Daftar Bersama reguler dan hasilnya adalah:
Daftar selesai dalam: 7793 milidetik. Bersamaan selesai dalam: 8064 milidetik.
sumber
RemoveAt(int index)
tidak pernah aman,Insert(int index, T item)
hanya aman untuk indeks == 0, kembalinyaIndexOf()
segera usang dll. Jangan mulai darithis[int]
.ReaderWriterLockSlim
dapat dibuat menjadi kebuntuan dengan mudah dengan menggunakanEnterUpgradeableReadLock()
secara bersamaan. Namun Anda tidak menggunakannya, Anda tidak membuat kunci dapat diakses dari luar, dan Anda tidak mis memanggil metode yang memasukkan kunci tulis sambil memegang kunci baca, jadi menggunakan kelas Anda tidak membuat kebuntuan lagi mungkin.var l = new ConcurrentList<string>(); /* ... */ l[0] += "asdf";
. Secara umum, kombo baca-tulis apa pun dapat membawa Anda ke masalah besar ketika dilakukan bersamaan. Itulah sebabnya struktur data bersamaan umumnya menyediakan metode bagi mereka, sepertiConcurrentDictionary
'sAddOrGet
dll NB konstan Anda (dan berlebihan karena anggota sudah ditandai seperti dengan garis bawah) pengulanganthis.
mengacaukan.ConcurrentList
(sebagai array yang dapat diubah ukurannya, bukan daftar yang ditautkan) tidak mudah untuk ditulis dengan operasi nonblocking. API-nya tidak diterjemahkan dengan baik ke versi "bersamaan".sumber
Alasan mengapa tidak ada ConcurrentList adalah karena secara fundamental tidak dapat ditulis. Alasan mengapa beberapa operasi penting dalam IList bergantung pada indeks, dan itu tidak akan berhasil. Sebagai contoh:
Efek yang penulis cari adalah memasukkan "anjing" sebelum "kucing", tetapi dalam lingkungan multithreaded, apa pun dapat terjadi pada daftar di antara dua baris kode tersebut. Misalnya, utas lainnya mungkin dilakukan
list.RemoveAt(0)
, menggeser seluruh daftar ke kiri, tetapi yang terpenting, catIndex tidak akan berubah. Dampaknya di sini adalah bahwaInsert
operasi akan benar-benar menempatkan "anjing" setelah kucing, bukan sebelumnya.Beberapa implementasi yang Anda lihat ditawarkan sebagai "jawaban" untuk pertanyaan ini bermaksud baik, tetapi seperti yang ditunjukkan di atas, mereka tidak menawarkan hasil yang dapat diandalkan. Jika Anda benar-benar menginginkan semantik mirip daftar dalam lingkungan multithreaded, Anda tidak bisa mencapainya dengan memasukkan kunci di dalamnya dalam metode implementasi daftar. Anda harus memastikan bahwa indeks apa pun yang Anda gunakan hidup sepenuhnya di dalam konteks kunci. Hasilnya adalah bahwa Anda dapat menggunakan Daftar di lingkungan multithreaded dengan penguncian yang benar, tetapi daftar itu sendiri tidak dapat dibuat ada di dunia itu.
Jika Anda merasa perlu daftar bersama, sebenarnya hanya ada dua kemungkinan:
Jika Anda memiliki ConcurrentBag dan berada dalam posisi di mana Anda harus melewatinya sebagai IList, maka Anda memiliki masalah, karena metode yang Anda panggil telah menentukan bahwa mereka mungkin mencoba melakukan sesuatu seperti yang saya lakukan di atas dengan kucing & anjing. Di sebagian besar dunia, apa artinya adalah bahwa metode yang Anda panggil sama sekali tidak dibangun untuk bekerja di lingkungan multi-utas. Itu berarti Anda baik refactor sehingga itu atau, jika Anda tidak bisa, Anda harus menanganinya dengan sangat hati-hati. Anda hampir pasti akan diminta untuk membuat koleksi Anda sendiri dengan kunci sendiri, dan memanggil metode yang menyinggung dalam kunci.
sumber
Dalam kasus di mana bacaan jauh lebih banyak daripada menulis, atau (betapapun sering) menulis tidak bersamaan , copy-on-write mungkin tepat.
Implementasi yang ditunjukkan di bawah ini adalah
var snap = _list; snap[snap.Count - 1];
tidak akan pernah (yah, kecuali untuk daftar kosong tentu saja) melempar, dan Anda juga mendapatkan pencacahan aman dengan semantik snapshot gratis .. bagaimana saya MENCINTAI kekekalan!Agar copy-on-write berfungsi, Anda harus menjaga agar struktur data Anda tetap tidak dapat diubah , yaitu tidak ada seorang pun yang boleh mengubahnya setelah Anda membuatnya tersedia untuk utas lainnya. Ketika Anda ingin memodifikasi, Anda
Kode
Pemakaian
Jika Anda memerlukan lebih banyak kinerja, ini akan membantu untuk menghilangkan metode, misalnya membuat satu metode untuk setiap jenis modifikasi (Tambah, Hapus, ...) yang Anda inginkan, dan hard code pointer fungsi
cloner
danop
.NB # 1 Adalah tanggung jawab Anda untuk memastikan tidak ada yang mengubah struktur data (yang seharusnya) tidak dapat diubah. Tidak ada yang bisa kita lakukan dalam implementasi generik untuk mencegah hal itu, tetapi ketika mengkhususkan diri untuk
List<T>
, Anda bisa menjaga terhadap modifikasi menggunakan List.AsReadOnly ()NB # 2 Berhati-hatilah dengan nilai-nilai dalam daftar. Pendekatan salin saat menulis di atas hanya melindungi keanggotaan daftar mereka, tetapi jika Anda tidak meletakkan string, tetapi beberapa objek yang dapat berubah di sana, Anda harus menjaga keamanan utas (mis. Mengunci). Tapi itu ortogonal untuk solusi ini dan misalnya penguncian nilai-nilai yang bisa berubah dapat dengan mudah digunakan tanpa masalah. Anda hanya perlu menyadarinya.
NB # 3 Jika struktur data Anda sangat besar dan Anda sering memodifikasinya, pendekatan copy-all-on-write mungkin menjadi penghalang baik dalam hal konsumsi memori maupun biaya penyalinan CPU yang terlibat. Dalam hal itu, Anda mungkin ingin menggunakan Koleksi Immutable MS sebagai gantinya.
sumber
System.Collections.Generic.List<t>
sudah aman untuk banyak pembaca. Mencoba membuatnya aman untuk banyak penulis tidak masuk akal. (Untuk alasan Henk dan Stephen sudah disebutkan)sumber
Beberapa orang menunjukkan beberapa poin barang (dan beberapa pemikiran saya):
Itu bukan jawaban. Ini hanya komentar yang tidak benar-benar cocok untuk tempat tertentu.
... Kesimpulan saya, Microsoft harus membuat beberapa perubahan mendalam pada "foreach" untuk membuat koleksi MultiThreaded lebih mudah digunakan. Juga harus mengikuti aturan penggunaan IEnumerator di sana. Sampai saat itu, kita dapat menulis MultiThreadList dengan mudah yang akan menggunakan iterator pemblokiran tetapi itu tidak akan mengikuti "IList". Sebagai gantinya, Anda harus mendefinisikan sendiri antarmuka "IListPersonnal" yang bisa gagal pada "insert", "remove" dan accessor acak (pengindeks) tanpa terkecuali. Tapi siapa yang mau menggunakannya jika tidak standar?
sumber
ConcurrentOrderedBag<T>
yang akan mencakup implementasi read-onlyIList<T>
, tetapi juga akan menawarkan metode yang sepenuhnya amanint Add(T value)
. Saya tidak melihat mengapaForEach
perubahan apa pun diperlukan. Meskipun Microsoft tidak secara eksplisit mengatakannya, praktik mereka menunjukkan bahwa sangat dapat diterima untukIEnumerator<T>
menghitung konten koleksi yang ada saat dibuat; pengecualian yang dimodifikasi untuk pengumpulan hanya diperlukan jika enumerator tidak dapat menjamin operasi bebas glitch.GetEnumerator
metode publik untuk membiarkan koleksi terkunci setelah kembali; desain seperti itu dapat dengan mudah menyebabkan kebuntuan. TheIEnumerable<T>
tidak memberikan indikasi apakah enumerasi dapat diharapkan untuk menyelesaikan bahkan jika koleksi diubah; yang terbaik yang bisa dilakukan adalah menulis metode sendiri sehingga mereka akan melakukannya, dan memiliki metode yang menerimaIEnumerable<T>
dokumen fakta yang hanya akan aman thread jikaIEnumerable<T>
mendukung enumerasi thread-aman.IEnumerable<T>
ada metode "Snapshot" dengan tipe pengembalianIEnumerable<T>
. Koleksi yang tidak bisa diubah bisa mengembalikan diri; koleksi terbatas dapat jika tidak ada yang menyalin dirinya sendiri keList<T>
atauT[]
dan memanggilGetEnumerator
itu. Beberapa koleksi tanpaSnapshot
batas dapat diimplementasikan , dan yang tidak dapat membuat pengecualian tanpa mencoba mengisi daftar dengan kontennya.Dalam mengeksekusi kode secara berurutan struktur data yang digunakan berbeda dari (juga ditulis) secara bersamaan mengeksekusi kode. Alasannya adalah bahwa kode berurutan menyiratkan urutan implisit. Namun kode bersamaan tidak menyiratkan urutan apa pun; lebih baik lagi itu menyiratkan kurangnya urutan yang ditentukan!
Karena ini, struktur data dengan urutan tersirat (seperti Daftar) tidak sangat berguna untuk memecahkan masalah bersamaan. Daftar menyiratkan pesanan, tetapi tidak jelas mendefinisikan apa pesanan itu. Karena itu, urutan eksekusi kode yang memanipulasi daftar akan menentukan (hingga taraf tertentu) urutan implisit daftar, yang bertentangan langsung dengan solusi konkuren yang efisien.
Ingat concurrency adalah masalah data, bukan masalah kode! Anda tidak dapat Menerapkan kode terlebih dahulu (atau menulis ulang kode sekuensial yang ada) dan mendapatkan solusi bersamaan yang dirancang dengan baik. Anda perlu merancang struktur data terlebih dahulu sambil tetap mengingat bahwa pemesanan implisit tidak ada dalam sistem bersamaan.
sumber
Lockless Copy and Write approach bekerja sangat baik jika Anda tidak berurusan dengan terlalu banyak item. Inilah kelas yang saya tulis:
contoh penggunaan: orders_BUY = CopyAndWriteList.Clear (orders_BUY);
sumber
Saya menerapkan yang mirip dengan Brian . Milik saya berbeda:
yield return
untuk menghasilkan enumerator.DoSync
danGetSync
metode yang memungkinkan interaksi berurutan yang memerlukan akses eksklusif ke daftar.Kode :
sumber
try
blokRemove
atau pengindeks pengindeks secara bersamaan?IList
semantik dalam skenario bersamaan terbatas. Saya menulis kode ini mungkin sebelum saya sampai pada realisasi itu. Pengalaman saya sama dengan penulis jawaban yang diterima: Saya mencobanya dengan apa yang saya ketahui tentang sinkronisasi dan IList <T> dan saya belajar sesuatu dengan melakukan itu.