Net 4.0'da Eşzamanlı Liste <T> yok mu?


198

System.Collections.ConcurrentNet 4.0'da yeni isim alanını görmek beni çok heyecanlandırdı ! Gördüğüm ConcurrentDictionary, ConcurrentQueue,ConcurrentStack , ConcurrentBagve BlockingCollection.

Gizemli bir şekilde eksik gibi görünen bir şey, ConcurrentList<T> . Bunu kendim mi yazmalıyım (ya da web'den çıkar :)

Burada bariz bir şeyi mi kaçırıyorum?



4
@RodrigoReis, ConcurrentBag <T> sıralanmamış bir koleksiyonken Liste <T> sıralanır.
Adam Calvet Bohl

4
Çok iş parçacıklı bir ortamda nasıl sipariş verebilirsiniz? Tasarım yoluyla elementlerin dizisini asla kontrol edemezdiniz.
Jeremy Holovacs

Bunun yerine bir kilit kullanın
Erik Bergstedt

dotnet kaynak kodunda aşağıdaki kodlara çok benzeyen ThreadSafeList.cs adlı bir dosya var. Ayrıca ReaderWriterLockSlim kullanır ve neden basit kilit (obj) yerine bunu anlamaya çalışıyordu?
colin lamarre

Yanıtlar:


166

Ben bunu bir deneyin a verdi süre önce de (: GitHub'dan ). Uygulamamın bazı problemleri vardı, buraya girmeyeceğim. Size daha da önemlisi, öğrendiklerimi anlatayım.

İlk olarak, bunun IList<T>kilitsiz ve iş parçacığı açısından güvenli bir şekilde tam olarak uygulanmasının bir yolu yoktur . Özel olarak, rasgele girmeler ve kaldırma işlemleri vardır değil (siz "hile" ve sadece bağlantılı liste çeşit kullanmak ve indeksleme emmek izin sürece, yani) ayrıca O noktasına (1) rastgele erişim unutmak sürece, işe gidiş.

Ne düşünce değerli olabilir bir iş parçacığı güvenli, sınırlı alt kümesi oldu IList<T>: özellikle, bir izin verecek bir Addve rastgele sağlamak salt okunur endeksi ile erişim (ama Insert, RemoveAtvb ve ayrıca hiçbir rastgele yazma erişimi).

Bu hedefi oldu benim ConcurrentList<T>uygulanması . Ancak performansını çok iş parçacıklı senaryolarda test ettiğimde, basitçe bir eklentiyi senkronize etmenin List<T>daha hızlı olduğunu buldum . Temel olarak, a eklemek List<T>zaten hızlı bir şekilde yıldırım; ilgili hesaplama adımlarının karmaşıklığı küçüktür (bir dizini arttırır ve bir dizideki bir öğeye atar; işte bu kadar ). Bununla ilgili herhangi bir kilit tartışmasını görmek için bir ton eşzamanlı yazara ihtiyacınız olacak ; ve o zaman bile, her yazının ortalama performansı, kilitsiz uygulama olsa da daha pahalıConcurrentList<T> .

Listenin dahili dizisinin kendisini yeniden boyutlandırması gereken nispeten nadir bir durumda, küçük bir maliyet ödersiniz. Nihayetinde bunun sadece bir toplama türü türünün mantıklı olacağı tek niş senaryosu olduğu sonucuna vardım ConcurrentList<T>: her çağrıda bir öğe eklemenin garantili düşük yükünü istediğinizde (yani, amortismana tabi bir performans hedefinin aksine).

Düşündüğünüz kadar basit bir sınıf değil.


52
Ve List<T>eski skool, monitör tabanlı senkronizasyon kullanan buna benzer bir şeye ihtiyacınız varsa , BCL'de SynchronizedCollection<T>gizlidir: msdn.microsoft.com/en-us/library/ms668265.aspx
LukeH

8
Küçük bir ekleme: yeniden boyutlandırma senaryosundan (mümkün olduğunca) kaçınmak için Capacity yapıcı parametresini kullanın.
Henk Holterman

2
ConcurrentListKazanmanın olacağı en büyük senaryo , listeye çok fazla aktivite eklenmediği, ancak birçok eşzamanlı okuyucu olduğunda olacaktır. Okuyucuların ek yükü tek bir bellek engeline indirgenebilir (ve okuyucular biraz eski verilerden endişe etmiyor olsalar bile ortadan kaldırılabilir).
supercat

2
@Kevin: ConcurrentList<T>Okuyucular, nispeten hafif ek yük ile herhangi bir kilitlemeye ihtiyaç duymadan tutarlı bir durum görmeleri garantilenecek şekilde inşa etmek oldukça önemsizdir . Liste örneğin 32-64 boyutundan genişlediğinde, size-32 dizisini koruyun ve yeni bir 64-boyutu dizisi oluşturun. Sonraki 32 öğenin her birini eklerken, yeni dizinin 32-63 yuvasına yerleştirin ve size-32 dizisinden eski bir öğeyi yenisine kopyalayın. 64. öğe eklenene kadar okuyucular 0-31 arasındaki öğeler için 32 numaralı diziye ve 32-63 arasındaki öğeler için 64 numaralı diziye bakacaktır.
supercat

2
64. öğe eklendikten sonra, size-32 dizisi 0-31 öğeleri almak için çalışmaya devam eder, ancak okuyucuların artık onu kullanmasına gerek yoktur. 0-63 arasındaki tüm öğeler için 64 numaralı diziyi ve 64-127 numaralı öğeler için 128 numaralı diziyi kullanabilirler. Hangi iki diziden hangisinin kullanılacağını seçme yükü ve istenirse bir bellek bariyeri, akla gelebilecek en verimli okuyucu-yazar kilidinin bile ek yükünden daha az olacaktır. Yazmalar muhtemelen kilitleri kullanmalıdır (özellikle her ekleme ile yeni bir nesne örneği oluşturmayı
düşünmezse kilitsiz

38

ConcurrentList'i ne için kullanırsınız?

Dişli bir dünyada Rastgele Erişim kabı kavramı, göründüğü kadar yararlı değildir. İfade

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

bir bütün olarak hala iş parçacığı için güvenli olmaz.

Bir ConcurrentList oluşturmak yerine, orada bulunanlarla çözümler oluşturmaya çalışın. En yaygın sınıflar ConcurrentBag ve özellikle de BlockingCollection'dır.


İyi bir nokta. Hala yaptığım şey biraz daha sıradan. Sadece bir IList <T> içine ConcurrentBag <T> atamaya çalışıyorum. Özelliğimi IEnumerable <T> olarak değiştirebilirim, ancak sonra yapamam.
Alan

1
@Alan: Listeyi kilitlemeden bunu uygulamanın bir yolu yok. Zaten Monitorbunu yapmak için zaten kullanabileceğiniz için, eşzamanlı bir liste için bir neden yoktur.
Billy ONeal

6
@dcp - evet bu doğal olarak iş parçacığı için güvenli değil. ConcurrentDictionary bunu bir atomik işlem, AddOrUpdate, GetOrAdd, TryUpdate, vb gibi özel yöntemler vardır. Onlar hala ContainsKey var çünkü bazen sadece anahtar sözlüğü değiştirmeden orada olup olmadığını bilmek istiyorum (düşünüyorum HashSet)
Zarat

3
@dcp - ContainsKey kendi başına threadsafe'dir, örneğinizin (ContainsKey değil!) sadece bir yarış durumu vardır, çünkü ilk karara bağlı olarak ikinci bir çağrı yaparsınız, bu noktada zaten güncel olmayabilir.
Zarat

2
Henk, katılmıyorum. Çok faydalı olabileceği basit bir senaryo olduğunu düşünüyorum. İçinde çalışan iş parçacığı yazma arayüzü uygun şekilde arayüzü okuma ve güncelleme olacaktır. Sıralı bir şekilde öğe eklemek istiyorsanız, rastgele erişim yazma gerektirir. Verilere bir yığın ve görünüm de kullanabilirsiniz, ancak 2 koleksiyon bulundurmanız gerekir :-(.
Eric Ouellet

19

Zaten verilen büyük cevaplara gereken tüm saygıyla, iş parçacığı açısından güvenli bir IList istediğim zamanlar var. Gelişmiş veya süslü bir şey yok. Performans birçok durumda önemlidir, ancak bazen bu bir endişe kaynağı değildir. Evet, her zaman "TryGetValue" gibi yöntemler olmadan zorluklar olacaktır, ancak çoğu durumda sadece her şeyin etrafına kilit koymak konusunda endişelenmeye gerek kalmadan numaralandırabileceğim bir şey istiyorum. Ve evet, birisi muhtemelen benim uygulamada bir kilitlenme veya bir şey (sanırım) yol açabilecek ama dürüst olalım bazı kod "hata" bulabilirsiniz: Çok iş parçacığı söz konusu olduğunda, kodunuzu doğru yazmazsanız, yine de çıkmaza giriyor. Bunu göz önünde bulundurarak, bu temel ihtiyaçları sağlayan basit bir ConcurrentList uygulaması yapmaya karar verdim.

Ve değeri için: Normal List ve ConcurrentList'e 10.000.000 öğe ekleyerek temel bir test yaptım ve sonuçlar şunlardı:

Liste tamamlandı: 7793 milisaniye. Eşzamanlı bitti: 8064 milisaniye.

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
}

5
Tamam, eski cevap ama yine de: RemoveAt(int index)asla iş parçacığı için güvenli, Insert(int index, T item)sadece dizin == 0 için güvenli, dönüşü IndexOf()hemen modası geçmiş vb this[int]. Hakkında bile başlamayın .
Henk Holterman

2
Ve ihtiyacınız yok ve istemiyorum ~ Finalizer ().
Henk Holterman

2
Sen çıkmazdan olasılığını önlemeye vazgeçmiş demek - ve tek ReaderWriterLockSlimkilitlenmeye yapılabilir kolayca kullanarak EnterUpgradeableReadLock()eşzamanlı. Ancak bunu kullanmazsınız, kilidi dışarıdan erişilebilir hale getirmezsiniz ve örneğin bir okuma kilidini tutarken yazma kilidine giren bir yöntemi çağırmazsınız, bu nedenle sınıfınızı kullanmak artık kilitlenmez muhtemel.
Eugene Beresovsky

1
Eşzamanlı olmayan arayüz eşzamanlı erişim için uygun değildir. Örneğin aşağıdakiler atomik değildir var l = new ConcurrentList<string>(); /* ... */ l[0] += "asdf";. Genel olarak, herhangi bir okuma-yazma kombinasyonu aynı anda yapıldığında sizi derin bir belaya sokabilir. Eşzamanlı veri yapıları genellikle gibilerin için yöntemler temin nedeni budur ConcurrentDictionary'ın AddOrGetvs. NB Kişisel sabiti (ve üyeler zaten çizgi tarafından da böyle işaretlenmiş gereksiz olduğundan) tekrarı this.clutters.
Eugene Beresovsky

1
Teşekkürler Eugene. Ben "bu" koyar. NET Reflector ağır bir kullanıcıyım. statik olmayan tüm alanlarda. Bu şekilde, aynı şeyi tercih etmek için büyüdüm. Bu eşzamanlı olmayan arayüzün uygun olmadığı konusunda: Uygulamama karşı birden fazla eylem gerçekleştirmeye çalışmanın güvenilmez hale gelebileceğinden kesinlikle haklısınız. Ancak buradaki gereklilik, tek bir işlemin (toplama, kaldırma veya temizleme veya numaralandırma) koleksiyonu bozmadan gerçekleştirilebilmesidir. Temelde her şeyin etrafına kilit ifadeleri koyma ihtiyacını ortadan kaldırır.
Brian Booth

11

ConcurrentList(yeniden boyutlandırılabilir bir dizi olarak, bağlantılı bir liste olarak değil), engelleme olmayan işlemlerle yazmak kolay değildir. API'sı, "eşzamanlı" bir sürüme iyi tercüme edilmiyor.


12
Sadece yazmak zor değil, aynı zamanda kullanışlı bir arayüz bulmak bile zor.
CodesInChaos

11

ConcurrentList olmamasının nedeni temelde yazılamamasıdır. Nedeni, IList'teki birkaç önemli operasyonun endekslere güvenmesidir ve bu sadece işe yaramaz. Örneğin:

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

Yazarın izlediği etki, "kedi" nin önüne "köpek" eklemektir, ancak çok iş parçacıklı bir ortamda, bu iki kod satırı arasındaki listeye her şey olabilir. Örneğin list.RemoveAt(0), tüm listeyi sola kaydırarak başka bir iş parçacığı yapabilir , ancak en önemlisi catIndex değişmez. Burada darbe olmasıdır Insertoperasyon aslında "köpek" koyacağız sonra önce değil, kedi.

Bu soruya "cevaplar" olarak teklif edildiğini düşündüğünüz birkaç uygulama iyi anlamlıdır, ancak yukarıda gösterildiği gibi güvenilir sonuçlar sunmazlar. Çok iş parçacıklı bir ortamda gerçekten liste benzeri anlambilim istiyorsanız, kilitleri içine koyarak oraya gidemezsiniz liste uygulaması yöntemlerinin. Kullandığınız tüm indekslerin tamamen kilit bağlamında kalmasını sağlamalısınız. Sonuç, bir Listeyi çok iş parçacıklı bir ortamda doğru kilitleme ile kullanabilmenizdir, ancak listenin kendisi bu dünyada var olamaz.

Eşzamanlı bir listeye ihtiyacınız olduğunu düşünüyorsanız, gerçekten sadece iki olasılık vardır:

  1. Gerçekten ihtiyacınız olan bir ConcurrentBag
  2. Belki de bir Liste ve kendi eşzamanlılık kontrolünüzle uygulanan kendi koleksiyonunuzu oluşturmanız gerekir.

Bir ConcurrentBag varsa ve bir IList olarak geçmeniz gereken bir konumda iseniz, o zaman bir sorun var, çünkü çağırdığınız yöntem, kedi ile yukarıda yaptığım gibi bir şey yapmaya çalışabileceklerini belirtti. köpek. Çoğu dünyada, bunun anlamı, çağırdığınız yöntemin çok iş parçacıklı bir ortamda çalışmak için tasarlanmamış olmasıdır. Bu, ya onu yeniden düzenlediğiniz anlamına gelir, ya da yapamazsanız, çok dikkatli bir şekilde ele almanız gerekecektir. Neredeyse kesinlikle kendi kilitleri ile kendi koleksiyonunuzu oluşturmanız ve bir kilit içinde rahatsız edici yöntemi çağırmanız istenecektir.


5

Durumlarda yazma outnumber ölçüde okur ya da (ancak sık) yazıyor bırlıkte verilmemektedir bir, kopya üzerinde yazma yaklaşımı uygun olabilir.

Aşağıda gösterilen uygulama

  • lockless
  • eşzamanlı değişiklikler devam ederken bile eşzamanlı okumalar için son derece hızlı - ne kadar sürecek olursa olsun
  • çünkü "anlık görüntüler" değişmez, kilitsiz atomisite mümkün, yani var snap = _list; snap[snap.Count - 1];asla (tabii ki, boş bir liste hariç) atmak asla, ve ayrıca ücretsiz anlık görüntü semantik ile iplik güvenli numaralandırma olsun .. nasıl değişmezlik seviyorum!
  • genel olarak uygulanan için, ilgili herhangi bir veri yapısı ve modifikasyon her tür
  • Ölü basit , yani testi kolay, hata ayıklama, kodu okuyarak doğrulama
  • Net 3.5'te kullanılabilir

Yazarken kopyalamanın işe yaraması için, veri yapılarınızı etkili bir şekilde değişmez tutmalısınız , yani başka iş parçacıkları için kullanılabilir hale getirdikten sonra hiç kimsenin bunları değiştirmesine izin verilmez. Değiştirmek istediğinizde,

  1. yapıyı klonla
  2. klonda değişiklik yapma
  3. değiştirilmiş klon referansında atomik değişim

kod

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;
        }
    }
}

kullanım

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

Daha fazla performansa ihtiyacınız varsa, yöntemin ungenerify edilmesine yardımcı olur, örneğin istediğiniz her değişiklik türü için (Add, Remove, ...) bir yöntem oluşturun ve işlev işaretleyicilerini clonerve sabit kodlayın op.

Not # 1 Hiç kimsenin (sözde) değişmez veri yapısını değiştirmediğinden emin olmak sizin sorumluluğunuzdadır. Bunu önlemek için genel bir uygulamada yapabileceğimiz hiçbir şey yoktur , ancak uzmanlaşırken List.AsReadOnly ()List<T> kullanarak değişikliklere karşı koruma sağlayabilirsiniz.

Not # 2 Listedeki değerlere dikkat edin. Yukarıdaki yazma yaklaşımı yalnızca liste üyeliğini korur, ancak dizeleri değil, oraya başka değişken nesneleri koyarsanız, iplik güvenliğine dikkat etmelisiniz (örn. Kilitleme). Ancak bu, bu çözüme diktir ve örneğin değişebilir değerlerin kilitlenmesi sorunsuz bir şekilde kullanılabilir. Sadece farkında olmalısın.

Not # 3 Veri yapınız çok büyükse ve onu sık sık değiştiriyorsanız, tümüne yazarken kopyala yaklaşımı hem bellek tüketimi hem de dahil olan kopyalamanın CPU maliyeti açısından engelleyici olabilir. Bu durumda, MS'nin Bağlanabilir Koleksiyonlarını kullanmak isteyebilirsiniz .


3

System.Collections.Generic.List<t>zaten birden çok okuyucu için güvenli. Birden fazla yazar için iş parçacığını güvenli hale getirmeye çalışmak mantıklı olmaz. (Henk ve Stephen'ın daha önce bahsettiği nedenlerden dolayı)


Bir listeye 5 iş parçacığı ekleyebileceğim bir senaryo göremiyor musunuz? Bu şekilde listenin, tümü sona ermeden bile kayıt biriktirdiğini görebilirsiniz.
Alan

9
@Alan - bu bir ConcurrentQueue, ConcurrentStack veya ConcurrentBag olacaktır. Bir ConcurrentList'i anlamak için, kullanılabilir sınıfların yeterli olmadığı bir kullanım örneği sağlamalısınız. Endekslerdeki elemanlar eşzamanlı kaldırmalar yoluyla rastgele değiştiğinde neden dizinli erişim istediğimi anlamıyorum. Ve "kilitli" bir okuma için mevcut eşzamanlı sınıfların anlık görüntülerini alabilir ve bunları bir listeye koyabilirsiniz.
Zarat

Haklısın - Dizine alınmış erişim istemiyorum. Genelde IList <T> 'i yeni öğeler için ekleyebileceğim bir IEnumerable için proxy olarak kullanıyorum. Aslında sorunun kaynağı budur.
Alan

@Alan: O zaman bir liste değil sıra istersiniz.
Billy ONeal

3
Bence yanılıyorsun. Çok sayıda okuyucu için güvenli demek aynı anda yazamayacağınız anlamına gelmez. Yazma da silme anlamına gelir ve yineleme sırasında silerseniz bir hata alırsınız.
Eric Ouellet

2

Bazı insanlar bazı mal puanlarına (ve bazı düşüncelerime) dikkat çekti:

  • Rastgele erişiciye (dizinleyici) erişememek delilik gibi görünebilir, ancak bana göre iyi görünüyor. Yalnızca çok iş parçacıklı koleksiyonlarda Dizin Oluşturucu ve Sil gibi başarısız olabilecek birçok yöntem olduğunu düşünmeniz gerekir. Yazma erişimcisi için "başarısız" veya sadece "sonunda ekle" gibi hata (yedek) eylemini de tanımlayabilirsiniz.
  • Bunun nedeni her zaman çok iş parçacıklı bir bağlamda kullanılacak çok iş parçacıklı bir koleksiyon olması değildir. Veya sadece bir yazar ve bir okuyucu tarafından da kullanılabilir.
  • Dizinleyiciyi güvenli bir şekilde kullanabilmenin bir başka yolu, eylemleri kökünü kullanarak (halka açıksa) koleksiyonun bir kilidine sarmak olabilir.
  • Birçok kişi için rootLock'u görünür yapmak "İyi uygulama" agaist olur. Bu noktadan% 100 emin değilim çünkü gizli ise kullanıcıya çok fazla esneklik kaldırırsınız. Her zaman hatırlamak zorundayız ki, çoklu iş parçacığının programlanması hiç kimse için değildir. Her türlü yanlış kullanımı önleyemeyiz.
  • Microsoft, Çok iş parçacıklı koleksiyonun doğru kullanımını tanıtmak için bazı çalışmalar yapmalı ve yeni bir standart tanımlamalıdır. Önce IEnumerator bir moveNext olmamalı, ancak true veya false döndüren ve T tipi bir çıkış parametresi alan bir GetNext'e sahip olmalıdır (bu şekilde yineleme artık engellemeyecektir). Ayrıca, Microsoft zaten foreach içinde "using" kullanır, ancak bazen IEnumerator'ı "using" (toplama görünümünde ve muhtemelen daha fazla yerde bir hata) ile sarmadan doğrudan kullanır. Bu hata, güvenli yineleyici için iyi potansiyeli ortadan kaldırır ... Toplayıcıyı kurucuda kilitler ve Dispose yönteminde kilidini açar - engelleme foreach yöntemi için.

Bu bir cevap değil. Bu sadece belirli bir yere gerçekten uymayan yorumlar.

... Sonuç olarak, Microsoft, MultiThreaded koleksiyonunun kullanımını kolaylaştırmak için "foreach" üzerinde bazı derin değişiklikler yapmak zorunda. Ayrıca burada IEnumerator kullanım kurallarına uymak zorundadır. O zamana kadar, bir engelleme yineleyicisi kullanan ancak "IList" i takip etmeyen bir MultiThreadList yazabiliriz. Bunun yerine, "insert", "remove" ve rasgele erişimci (indexer) üzerinde istisnasız başarısız olabilecek kendi "IListPersonnal" arabirimini tanımlamanız gerekecektir. Ama standart değilse kim kullanmak ister?


Biri kolayca ConcurrentOrderedBag<T>okunabilir bir uygulama içerir IList<T>, ancak aynı zamanda tamamen iş parçacığı için güvenli bir int Add(T value)yöntem de sunabilir . Neden herhangi bir ForEachdeğişikliğe ihtiyaç duyulacağını anlamıyorum . Microsoft açıkça söylemese de, uygulamaları IEnumerator<T>, oluşturulduğunda var olan koleksiyon içeriklerini numaralandırmanın mükemmel kabul edilebilir olduğunu; koleksiyon değiştirilmiş istisna sadece numaralandırıcı hatasız çalışmayı garanti edemezse gereklidir.
supercat

Bir MT koleksiyonu aracılığıyla yinelendiğinde, tasarım şekli, dediğin gibi bir istisnaya yol açabilir ... Hangisini bilmiyorum. Tüm istisnaları yakalar mısınız? Kendi kitabımda istisna istisnadır ve normal kod yürütülmesinde meydana gelmemelidir. Aksi takdirde, istisnayı önlemek için, eşzamanlılık nedeniyle istisnanın oluşmasını önlemek için koleksiyonu kilitlemeniz veya bir kopya almanız (güvenli bir şekilde-yani kilitlemeniz) veya koleksiyonda çok karmaşık bir mekanizma uygulamanız gerekir. Benim olsa da, her gerçekleşirken koleksiyon kilitlemek ve ilgili kodu eklemek IIumeratorMT eklemek güzel olurdu ...
Eric Ouellet

Oluşabilecek bir diğer şey, bir yineleyici aldığınızda koleksiyonu kilitleyebileceğiniz ve yineleyiciniz GC toplandığında koleksiyonun kilidini açabilmenizdir. Microsfot'a göre, zaten IEnumerable'ın IDisposable olup olmadığını kontrol ediyorlar ve eğer bir ForEach'ın sonunda GC'yi çağırıyorlar. Asıl sorun, GC'yi çağırmadan IEnumerable'ı başka bir yerde kullanmalarıdır, o zaman buna güvenemezsiniz. IEnumerable etkinleştirme kilidi için yeni bir açık MT arabirimine sahip olmak sorunu en azından bir kısmını çözecektir. (İnsanların aramamasını engellemez).
Eric Ouellet

Genel bir GetEnumeratoryöntemin bir koleksiyonu geri döndükten sonra kilitli bırakması çok kötü bir biçimdir ; bu tür tasarımlar kolayca kilitlenmeye yol açabilir. IEnumerable<T>Bir numaralandırma bir koleksiyon modifiye bile tamamlanması beklenebilir dair hiçbir belirti sağlar; yapabileceği en iyi şey, kendi yöntemlerini yazmaktır, böylece bunu yapmaları ve IEnumerable<T>yalnızca iş IEnumerable<T>parçacığı için güvenli numaralandırmayı desteklemesi durumunda iş parçacığı açısından güvenli olacağı gerçeğini kabul eden yöntemlere sahip olmaları gerekir .
supercat

IEnumerable<T>Dönüş türünde bir "Anlık Görüntü" yöntemi içermiş olsaydı, en yararlı olurdu IEnumerable<T>. Değişmez koleksiyonlar kendilerini geri getirebilirler; sınırlı bir koleksiyon başka hiçbir şey kendini bir List<T>veya 'ya kopyalamaz T[]ve bunu çağırmaz GetEnumerator. Bazı sınırsız koleksiyonlar uygulanabilir Snapshotve içeriği ile bir liste doldurmaya çalışmadan istisna atamayacak olanlar.
supercat

1

Kodun ardışık olarak yürütülmesinde kullanılan veri yapıları, eşzamanlı olarak yürütülen koddan (iyi yazılmış) farklıdır. Bunun nedeni, sıralı kodun örtük düzen anlamına gelmesidir. Bununla birlikte, eşzamanlı kod herhangi bir sipariş anlamına gelmez; daha iyi henüz herhangi bir tanımlanmış sipariş eksikliği anlamına gelir!

Bu nedenle, zımni sıraya sahip veri yapıları (Liste gibi), eşzamanlı problemlerin çözümü için çok yararlı değildir. Bir liste düzeni ifade eder, ancak bu siparişin ne olduğunu açıkça tanımlamaz. Bu nedenle, listeyi işleyen kodun yürütme sırası, etkin bir eşzamanlı çözümle doğrudan çakışan listenin örtülü sırasını (bir dereceye kadar) belirler.

Eşzamanlılığın bir kod sorunu değil bir veri sorunu olduğunu unutmayın! Önce kodu uygulayamaz (veya mevcut sıralı kodu yeniden yazamaz) ve iyi tasarlanmış bir eşzamanlı çözüm elde edemezsiniz. Eşzamanlı bir sistemde örtük sıralamanın olmadığını akılda tutarak, önce veri yapılarını tasarlamanız gerekir.


1

Kilitsiz Kopyala ve Yaz yaklaşımı, çok fazla öğe ile ilgilenmiyorsanız harika çalışır. İşte yazdığım bir sınıf:

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;
    }

}

örnek kullanım: orders_BUY = CopyAndWriteList.Clear (orders_BUY);


Kilitlemek yerine listenin bir kopyasını oluşturur, listeyi değiştirir ve referansı yeni listeye ayarlar. Bu nedenle, yinelenen diğer konular herhangi bir soruna neden olmaz.
Rob The Quant

0

Brian'ınkine benzer bir tane uyguladım . Benimki farklı:

  • Diziyi doğrudan yönetiyorum.
  • Deneme bloğunun içindeki kilitlere girmem.
  • kullanırım yield returnBir numaralandırıcı üretmek için .
  • Kilit özyinelemeyi destekliyorum. Bu, yineleme sırasında listeden okumalara izin verir.
  • Mümkün olduğunda yükseltilebilir okuma kilitleri kullanıyorum.
  • DoSyncve GetSynclisteye özel erişim gerektiren sıralı etkileşimlere izin veren yöntemler.

Kod :

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();
    }
}

İki iş parçacığı aynı anda trybloğun başlangıcına Removeveya dizin oluşturucuya girerse ne olur ?
James

@James mümkün görünmüyor. Açıklamaları msdn.microsoft.com/en-us/library/… adresinde okuyun . Bu kodu çalıştırdığınızda, bu kilidi asla 2 kez girmeyeceksiniz
Ronnie Overby

@Ronny Overby: İlginç. Bu göz önüne alındığında, UpgradableReadLock'u yükseltilebilir okuma kilidi ve yazma kilidi arasındaki zamanda yapılan tek işlemin - herhangi bir kilit alma yükünün çok daha fazla olduğu tüm işlevlerden kaldırırsanız bunun daha iyi performans göstereceğinden şüpheleniyorum. parametrenin, sadece yazma kilidinin içinde bu denetimi yapmanın daha iyi performans göstereceği aralığın dışında olup olmadığını kontrol edin.
James

Bu sınıf da çok yararlı görünmüyor, çünkü ofset tabanlı işlevler (çoğu), herhangi bir harici kilitleme şeması olmadığı sürece gerçekten güvenli bir şekilde kullanılamaz çünkü koleksiyon nereye koyacağınıza karar verdiğinizde veya bir şey alın ve ne zaman gerçekten olsun.
James

1
IListEşzamanlı senaryolarda anlambilimin faydasının en iyi şekilde sınırlı olduğunu bildiğimi söyleyerek kayıtlara geçmek istedim . Muhtemelen bu aydınlanmaya gelmeden önce bu kodu yazdım. Deneyimlerim kabul edilen cevabın yazarı ile aynı: Senkronizasyon ve IList <T> hakkında bildiklerimi denedim ve bunu yaparak bir şeyler öğrendim.
Ronnie Overby
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.