Tidak Ada Daftar Berurutan <T> dalam .Net 4.0?

198

Saya sangat senang melihat System.Collections.Concurrentnamespace baru di. Net 4.0, cukup bagus! Aku pernah melihat ConcurrentDictionary, ConcurrentQueue, ConcurrentStack, ConcurrentBagdan 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?

Alan
sumber
7
ConcurrentBag <T> ( msdn.microsoft.com/pt-br/library/… )
Rodrigo Reis
4
@RodrigoReis, ConcurrentBag <T> adalah koleksi yang tidak terurut, sedangkan Daftar <T> dipesan.
Adam Calvet Bohl
4
Bagaimana mungkin Anda memiliki koleksi yang dipesan di lingkungan multithreaded? Anda tidak akan pernah bisa mengontrol urutan elemen, dengan desain.
Jeremy Holovacs
Gunakan Kunci sebagai gantinya
Erik Bergstedt
ada file bernama ThreadSafeList.cs dalam kode sumber dotnet yang terlihat seperti beberapa kode di bawah ini. Ia menggunakan ReaderWriterLockSlim juga dan berusaha mencari tahu mengapa menggunakannya bukan kunci sederhana (obj)?
colin lamarre

Jawaban:

166

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 memungkinkan Adddan menyediakan random read-only akses dengan indeks (tapi tidak ada Insert, RemoveAt, dll, dan juga tidak ada random write akses).

Ini adalah tujuan implementasi sayaConcurrentList<T> . Tetapi ketika saya menguji kinerjanya dalam skenario multithreaded, saya menemukan bahwa hanya menyinkronkan menambahkan ke List<T>lebih cepat . Pada dasarnya, menambahkan ke List<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 di ConcurrentList<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.

Dan Tao
sumber
52
Dan jika Anda memerlukan sesuatu yang mirip dengan List<T>yang menggunakan sinkronisasi skool-tua berbasis monitor, ada yang SynchronizedCollection<T>tersembunyi di BCL: msdn.microsoft.com/en-us/library/ms668265.aspx
LukeH
8
Satu tambahan kecil: gunakan parameter konstruktor Kapasitas untuk menghindari (sebanyak mungkin) skenario pengubahan ukuran.
Henk Holterman
2
Skenario terbesar di mana a ConcurrentListakan 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).
supercat
2
@Kevin: Cukup sepele untuk membuat a 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.
supercat
2
Setelah item ke-64 ditambahkan, array ukuran-32 masih akan berfungsi untuk mengambil item 0-31, tetapi pembaca tidak perlu lagi menggunakannya. Mereka dapat menggunakan array ukuran-64 untuk semua item 0-63, dan array ukuran-128 untuk item 64-127. Overhead pemilihan salah satu dari dua array yang akan digunakan, ditambah penghalang memori jika diinginkan, akan lebih kecil daripada overhead bahkan kunci pembaca-penulis yang paling efisien yang bisa dibayangkan. Menulis mungkin harus menggunakan kunci (bebas kunci akan mungkin, terutama jika seseorang tidak keberatan membuat instance objek baru dengan setiap penyisipan, tetapi kunci harus murah.
supercat
38

Untuk apa Anda menggunakan ConcurrentList?

Konsep wadah Akses Acak di dunia berulir tidak berguna seperti yang terlihat. Pernyataan

  if (i < MyConcurrentList.Count)  
      x = MyConcurrentList[i]; 

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.

Henk Holterman
sumber
Poin yang bagus. Namun apa yang saya lakukan sedikit lebih biasa. Saya hanya mencoba untuk menetapkan ConcurrentBag <T> menjadi IList <T>. Saya bisa mengalihkan properti saya ke IEnumerable <T>, tapi kemudian saya tidak bisa. Menambahkan barang ke sana.
Alan
1
@Lan: Tidak ada cara untuk mengimplementasikannya tanpa mengunci daftar. Karena Anda sudah dapat menggunakannya Monitoruntuk melakukan itu, tidak ada alasan untuk daftar bersamaan.
Billy ONeal
6
@dcp - ya ini pada dasarnya tidak aman. ConcurrentDictionary memiliki metode khusus yang melakukan ini dalam satu operasi atom, seperti AddOrUpdate, GetOrAdd, TryUpdate, dll. Mereka masih memiliki ContainsKey karena kadang-kadang Anda hanya ingin tahu apakah kuncinya ada tanpa memodifikasi kamus (think HashSet)
Zarat
3
@dcp - ContainsKey adalah threadsafe dengan sendirinya, contoh Anda (bukan ContainsKey!) hanya memiliki kondisi balapan karena Anda melakukan panggilan kedua tergantung pada keputusan pertama, yang mungkin pada saat itu sudah ketinggalan zaman.
Zarat
2
Henk, aku tidak setuju. Saya pikir ada skenario sederhana di mana ini bisa sangat berguna. Pekerja menulis di dalamnya akan thread UI membaca dan memperbarui antarmuka yang sesuai. Jika Anda ingin menambahkan item dengan cara diurutkan, itu akan memerlukan akses tulis acak. Anda juga bisa menggunakan tumpukan dan tampilan ke data tetapi Anda harus memelihara 2 koleksi :-(.
Eric Ouellet
19

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.

public class ConcurrentList<T> : IList<T>, IDisposable
{
    #region Fields
    private readonly List<T> _list;
    private readonly ReaderWriterLockSlim _lock;
    #endregion

    #region Constructors
    public ConcurrentList()
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>();
    }

    public ConcurrentList(int capacity)
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>(capacity);
    }

    public ConcurrentList(IEnumerable<T> items)
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>(items);
    }
    #endregion

    #region Methods
    public void Add(T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Add(item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public void Insert(int index, T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Insert(index, item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public bool Remove(T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            return this._list.Remove(item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public void RemoveAt(int index)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.RemoveAt(index);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public int IndexOf(T item)
    {
        try
        {
            this._lock.EnterReadLock();
            return this._list.IndexOf(item);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public void Clear()
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Clear();
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public bool Contains(T item)
    {
        try
        {
            this._lock.EnterReadLock();
            return this._list.Contains(item);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        try
        {
            this._lock.EnterReadLock();
            this._list.CopyTo(array, arrayIndex);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new ConcurrentEnumerator<T>(this._list, this._lock);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return new ConcurrentEnumerator<T>(this._list, this._lock);
    }

    ~ConcurrentList()
    {
        this.Dispose(false);
    }

    public void Dispose()
    {
        this.Dispose(true);
    }

    private void Dispose(bool disposing)
    {
        if (disposing)
            GC.SuppressFinalize(this);

        this._lock.Dispose();
    }
    #endregion

    #region Properties
    public T this[int index]
    {
        get
        {
            try
            {
                this._lock.EnterReadLock();
                return this._list[index];
            }
            finally
            {
                this._lock.ExitReadLock();
            }
        }
        set
        {
            try
            {
                this._lock.EnterWriteLock();
                this._list[index] = value;
            }
            finally
            {
                this._lock.ExitWriteLock();
            }
        }
    }

    public int Count
    {
        get
        {
            try
            {
                this._lock.EnterReadLock();
                return this._list.Count;
            }
            finally
            {
                this._lock.ExitReadLock();
            }
        }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }
    #endregion
}

    public class ConcurrentEnumerator<T> : IEnumerator<T>
{
    #region Fields
    private readonly IEnumerator<T> _inner;
    private readonly ReaderWriterLockSlim _lock;
    #endregion

    #region Constructor
    public ConcurrentEnumerator(IEnumerable<T> inner, ReaderWriterLockSlim @lock)
    {
        this._lock = @lock;
        this._lock.EnterReadLock();
        this._inner = inner.GetEnumerator();
    }
    #endregion

    #region Methods
    public bool MoveNext()
    {
        return _inner.MoveNext();
    }

    public void Reset()
    {
        _inner.Reset();
    }

    public void Dispose()
    {
        this._lock.ExitReadLock();
    }
    #endregion

    #region Properties
    public T Current
    {
        get { return _inner.Current; }
    }

    object IEnumerator.Current
    {
        get { return _inner.Current; }
    }
    #endregion
}
Brian Booth
sumber
5
OK, jawaban lama tapi tetap: RemoveAt(int index)tidak pernah aman, Insert(int index, T item)hanya aman untuk indeks == 0, kembalinya IndexOf()segera usang dll. Jangan mulai dari this[int].
Henk Holterman
2
Dan Anda tidak perlu dan tidak ingin ~ Finalizer ().
Henk Holterman
2
Anda mengatakan bahwa Anda telah menyerah untuk mencegah kemungkinan kebuntuan - dan satu ReaderWriterLockSlimdapat dibuat menjadi kebuntuan dengan mudah dengan menggunakan EnterUpgradeableReadLock()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.
Eugene Beresovsky
1
Antarmuka non-konkuren tidak sesuai untuk akses bersamaan. Misalnya berikut ini bukan atom 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, seperti ConcurrentDictionary's AddOrGetdll NB konstan Anda (dan berlebihan karena anggota sudah ditandai seperti dengan garis bawah) pengulangan this.mengacaukan.
Eugene Beresovsky
1
Terima kasih Eugene. Saya adalah pengguna berat .NET Reflector yang menempatkan "ini." pada semua bidang non-statis. Karena itu, saya semakin menyukai hal yang sama. Mengenai antarmuka non-konkuren ini yang tidak sesuai: Anda benar bahwa mencoba melakukan beberapa tindakan terhadap implementasi saya bisa menjadi tidak dapat diandalkan. Tetapi persyaratan di sini adalah hanya bahwa tindakan tunggal (menambah, atau menghapus, atau menghapus, atau enumerasi) dapat dilakukan tanpa merusak koleksi. Ini pada dasarnya menghilangkan kebutuhan untuk menempatkan pernyataan kunci di sekitar segalanya.
Brian Booth
11

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".

Stephen Cleary
sumber
12
Tidak hanya sulit untuk menulis, bahkan sulit untuk menemukan antarmuka yang berguna.
CodesInChaos
11

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:

int catIndex = list.IndexOf("cat");
list.Insert(catIndex, "dog");

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 bahwa Insertoperasi 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:

  1. Yang Anda butuhkan adalah ConcurrentBag
  2. Anda perlu membuat koleksi Anda sendiri, mungkin diimplementasikan dengan Daftar dan kontrol konkurensi Anda sendiri.

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.

Steve Benz
sumber
5

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

  • tidak terkunci
  • sangat cepat untuk membaca bersamaan , bahkan saat modifikasi bersamaan sedang berlangsung - tidak peduli berapa lama waktu yang dibutuhkan
  • karena "snapshots" tidak dapat diubah, atomicity tanpa kunci adalah mungkin, yaitu 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!
  • diimplementasikan secara umum , berlaku untuk setiap struktur data dan semua jenis modifikasi
  • sederhana , yaitu mudah untuk menguji, men-debug, memverifikasi dengan membaca kode
  • dapat digunakan dalam. Net 3.5

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

  1. mengkloning struktur
  2. melakukan modifikasi pada klon
  3. bertukar atom dalam referensi ke klon yang dimodifikasi

Kode

static class CopyOnWriteSwapper
{
    public static void Swap<T>(ref T obj, Func<T, T> cloner, Action<T> op)
        where T : class
    {
        while (true)
        {
            var objBefore = Volatile.Read(ref obj);
            var newObj = cloner(objBefore);
            op(newObj);
            if (Interlocked.CompareExchange(ref obj, newObj, objBefore) == objBefore)
                return;
        }
    }
}

Pemakaian

CopyOnWriteSwapper.Swap(ref _myList,
    orig => new List<string>(orig),
    clone => clone.Add("asdf"));

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 clonerdan op.

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.

Eugene Beresovsky
sumber
3

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)

Billy ONeal
sumber
Anda tidak dapat melihat skenario di mana saya mungkin memiliki 5 utas yang ditambahkan ke daftar? Dengan cara ini Anda bisa melihat daftar mengumpulkan catatan bahkan sebelum semuanya berakhir.
Alan
9
@Alan - itu akan menjadi ConcurrentQueue, ConcurrentStack atau ConcurrentBag. Untuk memahami ConcurrentList, Anda harus menyediakan case-use di mana kelas yang tersedia tidak cukup. Saya tidak melihat mengapa saya ingin akses terindeks ketika elemen pada indeks secara acak dapat berubah melalui penghapusan bersamaan. Dan untuk bacaan "terkunci" Anda sudah dapat mengambil snapshot dari kelas bersamaan yang ada dan memasukkannya ke dalam daftar.
Zarat
Anda benar - saya tidak ingin akses terindeks. Saya biasanya menggunakan IList <T> sebagai proksi untuk IEnumerable yang saya dapat. Tambahkan (T) elemen baru. Dari situlah pertanyaan itu berasal.
Alan
@Lan: Maka Anda ingin antrian, bukan daftar.
Billy ONeal
3
Saya pikir kamu salah. Mengatakan: aman untuk banyak pembaca tidak berarti Anda tidak dapat menulis pada saat bersamaan. Menulis juga berarti menghapus dan Anda akan mendapatkan kesalahan jika Anda menghapus saat iterasi di dalamnya.
Eric Ouellet
2

Beberapa orang menunjukkan beberapa poin barang (dan beberapa pemikiran saya):

  • Itu bisa terlihat seperti gila untuk tidak dapat accesser acak (pengindeks) tetapi bagi saya tampaknya baik-baik saja. Anda hanya perlu berpikir bahwa ada banyak metode pada koleksi multi-utas yang bisa gagal seperti Indexer dan Hapus. Anda juga bisa mendefinisikan tindakan kegagalan (fallback) untuk aksesor tulis seperti "gagal" atau cukup "tambahkan di akhir".
  • Ini bukan karena itu adalah kumpulan multithreaded yang akan selalu digunakan dalam konteks multithreaded. Atau bisa juga digunakan oleh hanya satu penulis dan satu pembaca.
  • Cara lain untuk dapat menggunakan pengindeks dengan cara yang aman bisa dengan membungkus tindakan ke dalam kunci koleksi menggunakan root (jika dipublikasikan).
  • Bagi banyak orang, membuat rootLock dapat dilihat berarti "Praktik yang baik". Saya tidak 100% yakin tentang hal ini karena jika disembunyikan Anda menghapus banyak fleksibilitas kepada pengguna. Kita harus selalu ingat bahwa pemrograman multithread bukan untuk siapa pun. Kami tidak dapat mencegah segala jenis penggunaan yang salah.
  • Microsoft harus melakukan beberapa pekerjaan dan menetapkan beberapa standar baru untuk memperkenalkan penggunaan koleksi Multithreaded yang tepat. Pertama, IEnumerator seharusnya tidak memiliki moveNext tetapi harus memiliki GetNext yang mengembalikan true atau false dan mendapatkan parameter tipe T (dengan cara ini iterasi tidak akan memblokir lagi). Juga, Microsoft sudah menggunakan "menggunakan" secara internal di muka tetapi kadang-kadang menggunakan IEnumerator secara langsung tanpa membungkusnya dengan "menggunakan" (bug dalam tampilan koleksi dan mungkin di lebih banyak tempat) - Membungkus penggunaan IEnumerator adalah praktik yang disarankan oleh Microsoft. Bug ini menghilangkan potensi bagus untuk iterator aman ... Iterator yang mengunci koleksi di konstruktor dan membuka kunci pada metode Buang - untuk metode foreach pemblokiran.

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?

Eric Ouellet
sumber
Orang bisa dengan mudah menulis ConcurrentOrderedBag<T>yang akan mencakup implementasi read-only IList<T>, tetapi juga akan menawarkan metode yang sepenuhnya aman int Add(T value). Saya tidak melihat mengapa ForEachperubahan apa pun diperlukan. Meskipun Microsoft tidak secara eksplisit mengatakannya, praktik mereka menunjukkan bahwa sangat dapat diterima untuk IEnumerator<T>menghitung konten koleksi yang ada saat dibuat; pengecualian yang dimodifikasi untuk pengumpulan hanya diperlukan jika enumerator tidak dapat menjamin operasi bebas glitch.
supercat
Iterasi melalui koleksi MT, cara desainnya bisa mengarah, seperti yang Anda katakan, untuk pengecualian ... Mana yang saya tidak tahu. Apakah Anda akan menjebak semua pengecualian? Dalam buku saya sendiri pengecualian adalah pengecualian dan tidak boleh terjadi dalam pelaksanaan kode normal. Kalau tidak, untuk mencegah pengecualian, Anda harus mengunci koleksi atau mendapatkan salinan (dengan cara yang aman-yaitu kunci) atau menerapkan mekanisme yang sangat kompleks dalam koleksi untuk mencegah pengecualian terjadi karena konkurensi. Meskipun saya adalah bahwa akan menyenangkan untuk menambahkan IEnumeratorMT yang akan mengunci koleksi sementara untuk setiap terjadi dan menambahkan kode terkait ...
Eric Ouellet
Hal lain yang juga bisa terjadi adalah ketika Anda mendapatkan iterator, Anda dapat mengunci koleksi dan ketika iterator Anda dikumpulkan GC Anda dapat membuka kunci koleksi. Menurut Microsfot mereka sudah memeriksa apakah IEnumerable juga merupakan IDisposable dan memanggil GC jika demikian pada akhir ForEach. Masalah utama adalah bahwa mereka juga menggunakan IEnumerable di tempat lain tanpa memanggil GC, Anda kemudian tidak dapat mengandalkan itu. Memiliki antarmuka MT baru yang jelas untuk kunci pengaktifan IEnumerable akan menyelesaikan masalah, setidaknya sebagian darinya. (Itu tidak akan mencegah orang untuk tidak menyebutnya).
Eric Ouellet
Ini adalah bentuk yang sangat buruk untuk GetEnumeratormetode publik untuk membiarkan koleksi terkunci setelah kembali; desain seperti itu dapat dengan mudah menyebabkan kebuntuan. The IEnumerable<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 menerima IEnumerable<T>dokumen fakta yang hanya akan aman thread jika IEnumerable<T>mendukung enumerasi thread-aman.
supercat
Apa yang paling membantu seandainya IEnumerable<T>ada metode "Snapshot" dengan tipe pengembalian IEnumerable<T>. Koleksi yang tidak bisa diubah bisa mengembalikan diri; koleksi terbatas dapat jika tidak ada yang menyalin dirinya sendiri ke List<T>atau T[]dan memanggil GetEnumeratoritu. Beberapa koleksi tanpa Snapshotbatas dapat diimplementasikan , dan yang tidak dapat membuat pengecualian tanpa mencoba mengisi daftar dengan kontennya.
supercat
1

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.

Cetak Biru41
sumber
1

Lockless Copy and Write approach bekerja sangat baik jika Anda tidak berurusan dengan terlalu banyak item. Inilah kelas yang saya tulis:

public class CopyAndWriteList<T>
{
    public static List<T> Clear(List<T> list)
    {
        var a = new List<T>(list);
        a.Clear();
        return a;
    }

    public static List<T> Add(List<T> list, T item)
    {
        var a = new List<T>(list);
        a.Add(item);
        return a;
    }

    public static List<T> RemoveAt(List<T> list, int index)
    {
        var a = new List<T>(list);
        a.RemoveAt(index);
        return a;
    }

    public static List<T> Remove(List<T> list, T item)
    {
        var a = new List<T>(list);
        a.Remove(item);
        return a;
    }

}

contoh penggunaan: orders_BUY = CopyAndWriteList.Clear (orders_BUY);

Rob The Quant
sumber
alih-alih mengunci, itu membuat salinan daftar, memodifikasi daftar dan mengatur referensi ke daftar baru. Jadi utas lainnya yang beriterasi tidak akan menyebabkan masalah.
Rob The Quant
0

Saya menerapkan yang mirip dengan Brian . Milik saya berbeda:

  • Saya mengelola array secara langsung.
  • Saya tidak memasukkan kunci di dalam blok coba.
  • saya menggunakan yield return untuk menghasilkan enumerator.
  • Saya mendukung rekursi kunci. Ini memungkinkan membaca dari daftar selama iterasi.
  • Saya menggunakan kunci baca yang dapat diupgrade jika memungkinkan.
  • DoSyncdan GetSyncmetode yang memungkinkan interaksi berurutan yang memerlukan akses eksklusif ke daftar.

Kode :

public class ConcurrentList<T> : IList<T>, IDisposable
{
    private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
    private int _count = 0;

    public int Count
    {
        get
        { 
            _lock.EnterReadLock();
            try
            {           
                return _count;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
    }

    public int InternalArrayLength
    { 
        get
        { 
            _lock.EnterReadLock();
            try
            {           
                return _arr.Length;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
    }

    private T[] _arr;

    public ConcurrentList(int initialCapacity)
    {
        _arr = new T[initialCapacity];
    }

    public ConcurrentList():this(4)
    { }

    public ConcurrentList(IEnumerable<T> items)
    {
        _arr = items.ToArray();
        _count = _arr.Length;
    }

    public void Add(T item)
    {
        _lock.EnterWriteLock();
        try
        {       
            var newCount = _count + 1;          
            EnsureCapacity(newCount);           
            _arr[_count] = item;
            _count = newCount;                  
        }
        finally
        {
            _lock.ExitWriteLock();
        }       
    }

    public void AddRange(IEnumerable<T> items)
    {
        if (items == null)
            throw new ArgumentNullException("items");

        _lock.EnterWriteLock();

        try
        {           
            var arr = items as T[] ?? items.ToArray();          
            var newCount = _count + arr.Length;
            EnsureCapacity(newCount);           
            Array.Copy(arr, 0, _arr, _count, arr.Length);       
            _count = newCount;
        }
        finally
        {
            _lock.ExitWriteLock();          
        }
    }

    private void EnsureCapacity(int capacity)
    {   
        if (_arr.Length >= capacity)
            return;

        int doubled;
        checked
        {
            try
            {           
                doubled = _arr.Length * 2;
            }
            catch (OverflowException)
            {
                doubled = int.MaxValue;
            }
        }

        var newLength = Math.Max(doubled, capacity);            
        Array.Resize(ref _arr, newLength);
    }

    public bool Remove(T item)
    {
        _lock.EnterUpgradeableReadLock();

        try
        {           
            var i = IndexOfInternal(item);

            if (i == -1)
                return false;

            _lock.EnterWriteLock();
            try
            {   
                RemoveAtInternal(i);
                return true;
            }
            finally
            {               
                _lock.ExitWriteLock();
            }
        }
        finally
        {           
            _lock.ExitUpgradeableReadLock();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        _lock.EnterReadLock();

        try
        {    
            for (int i = 0; i < _count; i++)
                // deadlocking potential mitigated by lock recursion enforcement
                yield return _arr[i]; 
        }
        finally
        {           
            _lock.ExitReadLock();
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    public int IndexOf(T item)
    {
        _lock.EnterReadLock();
        try
        {   
            return IndexOfInternal(item);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    private int IndexOfInternal(T item)
    {
        return Array.FindIndex(_arr, 0, _count, x => x.Equals(item));
    }

    public void Insert(int index, T item)
    {
        _lock.EnterUpgradeableReadLock();

        try
        {                       
            if (index > _count)
                throw new ArgumentOutOfRangeException("index"); 

            _lock.EnterWriteLock();
            try
            {       
                var newCount = _count + 1;
                EnsureCapacity(newCount);

                // shift everything right by one, starting at index
                Array.Copy(_arr, index, _arr, index + 1, _count - index);

                // insert
                _arr[index] = item;     
                _count = newCount;
            }
            finally
            {           
                _lock.ExitWriteLock();
            }
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();            
        }


    }

    public void RemoveAt(int index)
    {   
        _lock.EnterUpgradeableReadLock();
        try
        {   
            if (index >= _count)
                throw new ArgumentOutOfRangeException("index");

            _lock.EnterWriteLock();
            try
            {           
                RemoveAtInternal(index);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();            
        }
    }

    private void RemoveAtInternal(int index)
    {           
        Array.Copy(_arr, index + 1, _arr, index, _count - index-1);
        _count--;

        // release last element
        Array.Clear(_arr, _count, 1);
    }

    public void Clear()
    {
        _lock.EnterWriteLock();
        try
        {        
            Array.Clear(_arr, 0, _count);
            _count = 0;
        }
        finally
        {           
            _lock.ExitWriteLock();
        }   
    }

    public bool Contains(T item)
    {
        _lock.EnterReadLock();
        try
        {   
            return IndexOfInternal(item) != -1;
        }
        finally
        {           
            _lock.ExitReadLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {       
        _lock.EnterReadLock();
        try
        {           
            if(_count > array.Length - arrayIndex)
                throw new ArgumentException("Destination array was not long enough.");

            Array.Copy(_arr, 0, array, arrayIndex, _count);
        }
        finally
        {
            _lock.ExitReadLock();           
        }
    }

    public bool IsReadOnly
    {   
        get { return false; }
    }

    public T this[int index]
    {
        get
        {
            _lock.EnterReadLock();
            try
            {           
                if (index >= _count)
                    throw new ArgumentOutOfRangeException("index");

                return _arr[index]; 
            }
            finally
            {
                _lock.ExitReadLock();               
            }           
        }
        set
        {
            _lock.EnterUpgradeableReadLock();
            try
            {

                if (index >= _count)
                    throw new ArgumentOutOfRangeException("index");

                _lock.EnterWriteLock();
                try
                {                       
                    _arr[index] = value;
                }
                finally
                {
                    _lock.ExitWriteLock();              
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }

        }
    }

    public void DoSync(Action<ConcurrentList<T>> action)
    {
        GetSync(l =>
        {
            action(l);
            return 0;
        });
    }

    public TResult GetSync<TResult>(Func<ConcurrentList<T>,TResult> func)
    {
        _lock.EnterWriteLock();
        try
        {           
            return func(this);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void Dispose()
    {   
        _lock.Dispose();
    }
}
Ronnie Overby
sumber
Apa yang terjadi jika dua utas masuk ke awal tryblok Removeatau pengindeks pengindeks secara bersamaan?
James
@James yang sepertinya tidak mungkin. Baca komentar di msdn.microsoft.com/en-us/library/… . Menjalankan kode ini, Anda tidak akan pernah memasukkan kunci ke-2 kalinya: gist.github.com/ronnieoverby/59b715c3676127a113c3
Ronnie Overby
@Ronny Overby: Menarik. Mengingat hal itu, saya menduga bahwa ini akan berkinerja lebih baik jika Anda menghapus UpgradableReadLock dari semua fungsi di mana satu-satunya operasi dilakukan pada waktu antara kunci baca yang dapat diupgrade dan kunci tulis - biaya tambahan untuk mengambil segala jenis kunci jauh lebih banyak. daripada memeriksa untuk melihat apakah parameter di luar kisaran yang hanya melakukan pemeriksaan itu di dalam kunci tulis kemungkinan akan berkinerja lebih baik.
James
Kelas ini juga tampaknya tidak terlalu berguna, karena fungsi berbasis offset (kebanyakan dari mereka) tidak dapat benar-benar digunakan dengan aman kecuali jika ada beberapa skema penguncian eksternal karena koleksi dapat berubah antara ketika Anda memutuskan di mana harus meletakkan atau dapatkan sesuatu dari dan ketika Anda benar-benar mendapatkannya.
James
1
Saya ingin melanjutkan dengan mengatakan bahwa saya menyadari bahwa kegunaan IListsemantik 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.
Ronnie Overby