.NET Framework'te Eşzamanlı HashSet <T>?


151

Aşağıdaki sınıfa sahibim.

class Test{
    public HashSet<string> Data = new HashSet<string>();
}

Alan "Veri" farklı iş parçacıklarını değiştirmek gerekir, bu yüzden geçerli iş parçacığı güvenli uygulama hakkında bazı görüşler istiyorum.

class Test{
    public HashSet<string> Data = new HashSet<string>();

    public void Add(string Val){
            lock(Data) Data.Add(Val);
    }

    public void Remove(string Val){
            lock(Data) Data.Remove(Val);
    }
}

Doğrudan alana gitmek ve birden fazla iş parçacığının eşzamanlı erişiminden korumak için daha iyi bir çözüm var mı?


System.Collections.Concurrent
Aşağıdaki

8
Tabii ki, onu özel yap.
Hans Passant

3
Eşzamanlılık açısından, Veri alanının herkese açık olmasından başka yaptıklarınızla ilgili pek bir yanlış görmüyorum! Bu bir endişe ise ReaderWriterLockSlim kullanarak daha iyi okuma performansı elde edebilirsiniz. msdn.microsoft.com/en-us/library/…
Allan Elder

@AllanElder ReaderWriterLock, birden fazla okuyucu ve tek bir yazar olduğunda yardımcı olacaktır (verimli). OP için durumun böyle olup olmadığını bilmeliyiz
Sriram Sakthivel

2
Mevcut uygulama gerçekten 'eşzamanlı' değil :) Sadece iş parçacığı için güvenlidir.
undefined

Yanıtlar:


164

Uygulamanız doğru. Ne yazık ki .NET Framework yerleşik bir eşzamanlı hashset türü sağlamaz. Ancak, bazı geçici çözümler vardır.

ConcurrentDictionary (önerilir)

Bu ilki sınıfı ConcurrentDictionary<TKey, TValue>ad alanında kullanmaktır System.Collections.Concurrent. Bu durumda, değer anlamsızdır, bu yüzden basit byte(bellekte 1 bayt) kullanabiliriz.

private ConcurrentDictionary<string, byte> _data;

Tür, iş parçacığı açısından güvenli olduğundan ve size bir HashSet<T>anahtar ve değer farklı nesneler dışında aynı avantajları sağladığı için bu önerilen seçenektir .

Kaynak: Sosyal MSDN

ConcurrentBag

Yinelenen girişleri dikkate almazsanız, sınıfı bir ConcurrentBag<T>önceki sınıfın aynı ad alanında kullanabilirsiniz .

private ConcurrentBag<string> _data;

Kendinden uygulama

Son olarak, yaptığınız gibi, kilit veya .NET'in iş parçacığı için güvenli olmasını sağladığı diğer yolları kullanarak kendi veri türünüzü uygulayabilirsiniz. İşte harika bir örnek: .Net'te ConcurrentHashSet nasıl uygulanır?

Bu çözümün tek dezavantajı, türün HashSet<T>okuma işlemleri için bile resmi olarak eşzamanlı erişim olmamasıdır.

Bağlantılı yazının kodunu alıntıladım (aslında Ben Mosher tarafından yazılmıştır ).

using System;
using System.Collections.Generic;
using System.Threading;

namespace BlahBlah.Utilities
{
    public class ConcurrentHashSet<T> : IDisposable
    {
        private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
        private readonly HashSet<T> _hashSet = new HashSet<T>();

        #region Implementation of ICollection<T> ...ish
        public bool Add(T item)
        {
            _lock.EnterWriteLock();
            try
            {
                return _hashSet.Add(item);
            }
            finally
            {
                if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            }
        }

        public void Clear()
        {
            _lock.EnterWriteLock();
            try
            {
                _hashSet.Clear();
            }
            finally
            {
                if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            }
        }

        public bool Contains(T item)
        {
            _lock.EnterReadLock();
            try
            {
                return _hashSet.Contains(item);
            }
            finally
            {
                if (_lock.IsReadLockHeld) _lock.ExitReadLock();
            }
        }

        public bool Remove(T item)
        {
            _lock.EnterWriteLock();
            try
            {
                return _hashSet.Remove(item);
            }
            finally
            {
                if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            }
        }

        public int Count
        {
            get
            {
                _lock.EnterReadLock();
                try
                {
                    return _hashSet.Count;
                }
                finally
                {
                    if (_lock.IsReadLockHeld) _lock.ExitReadLock();
                }
            }
        }
        #endregion

        #region Dispose
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
                if (_lock != null)
                    _lock.Dispose();
        }
        ~ConcurrentHashSet()
        {
            Dispose(false);
        }
        #endregion
    }
}

DÜZENLEME:try Bir istisna atabilir ve finallybloklarda bulunan talimatları yürütebileceğinden , giriş kilidi yöntemlerini blokların üzerine taşıyın .


8
önemsiz değerleri olan bir sözlük bir listedir
Ralf

44
@Ralf Sıralanmamış olduğu için bir liste değil, bir set.
Servy

11
MSDN'nin "Koleksiyonlar ve Senkronizasyon (İş Parçacığı Güvenliği)" konusundaki oldukça kısa belgesine göre , System.Collections ve ilgili ad alanlarındaki sınıflar birden çok iş parçacığı tarafından güvenle okunabilir. Bu, HashSet'in birden çok iş parçacığı tarafından güvenle okunabileceği anlamına gelir.
Hank Schultz

7
@Oliver, bir başvuru, başvuru olsa bile giriş başına çok daha fazla bellek kullanır null(başvurunun 32 bit çalışma zamanında 4 bayt ve 64 bit çalışma zamanında 8 bayt gerekir). Bu nedenle, a byte, boş bir yapı veya benzerinin kullanılması bellek kapladığı alanı azaltabilir (veya çalışma zamanının verileri daha hızlı erişim için yerel bellek sınırlarındaki verileri hizalaması olmayabilir).
Lucero

4
Kendi kendine uygulama bir ConcurrentHashSet değil, bir ThreadSafeHashSet'tir. Bu 2 arasında büyük bir fark var ve bu yüzden Micorosft SynchronizedCollections'ı terk etti (insanlar yanlış anladı). "Eşzamanlı" olabilmek için GetOrAdd vb. İşlemler (sözlük gibi) uygulanmalı ya da ek kilitleme olmadan eşzamanlılık sağlanamaz. Sınıf dışında ek bir kilitleme gerekiyorsa, neden basit bir HashSet'i en baştan kullanmıyorsunuz?
George Mavritsakis

36

Bunun yerine bir sarma ConcurrentDictionaryveya üzerinde kilitleme HashSetben gerçek oluşturulan ConcurrentHashSetdayalı ConcurrentDictionary.

Bu uygulama, HashSeteşzamanlı IMO senaryolarında daha az mantıklı oldukları için, ayarlanmış işlemleri olmadan öğe başına temel işlemleri destekler :

var concurrentHashSet = new ConcurrentHashSet<string>(
    new[]
    {
        "hamster",
        "HAMster",
        "bar",
    },
    StringComparer.OrdinalIgnoreCase);

concurrentHashSet.TryRemove("foo");

if (concurrentHashSet.Contains("BAR"))
{
    Console.WriteLine(concurrentHashSet.Count);
}

Çıktı: 2

Sen Nuget mal alabilirsiniz burada ve GitHub üzerinde kaynağını görmek burada .


3
Bu kabul edilen cevap, harika bir uygulama olmalı
smirkingman

ConcurrentDictionary ile tutarlı olması için Add Trydd olarak yeniden adlandırılmamalıdır ?
Neo

8
@Neo Hayır ... kasten kullanıyor çünkü HashSet <T> aramak anlambilim, ekleme ve öğenin (doğru) eklenmiştir belirten bir boolean döndüren veya zaten (false) mevcuttu. msdn.microsoft.com/en-us/library/bb353005(v=vs.110).aspx
G-Mac

ISet<T>Arayüzü bo gerçek HashSet<T>anlambilim ile uygulamak gerekir ?
Nekromancer

1
@Nekromancer cevabımda söylediğim gibi, bu set yöntemlerini eşzamanlı bir uygulamada sağlamanın mantıklı olduğunu düşünmüyorum. Overlapsörneğin, örneğin çalışması boyunca kilitlenmesi veya zaten yanlış olabilecek bir yanıt sağlaması gerekir. Her iki seçenek de kötü IMO'dur (ve tüketiciler tarafından harici olarak eklenebilir).
i3arnon

21

Başka kimse bundan bahsetmediği için, sizin özel amacınız için uygun olan veya olmayan alternatif bir yaklaşım sunacağım:

Microsoft Bağlanabilir Koleksiyonlar

Arkadaki MS ekibinin bir blog gönderisinden :

Eşzamanlı olarak oluşturma ve çalıştırma her zamankinden daha kolay olsa da, temel sorunlardan biri hala var: değişebilir paylaşılan devlet. Birden çok iş parçacığından okumak genellikle çok kolaydır, ancak durumun güncellenmesi gerektiğinde, özellikle kilitleme gerektiren tasarımlarda çok daha zorlaşır.

Kilitlemenin bir alternatifi değişmez durumdan yararlanmaktır. Değişmez veri yapılarının asla değişmeyeceği garanti edilir ve böylece başka birinin parmaklarına basmaktan endişe etmeden farklı iplikler arasında serbestçe geçirilebilir.

Bu tasarım yeni bir sorun oluşturuyor: Her seferinde tüm durumu kopyalamadan durum değişikliklerini nasıl yönetiyorsunuz? Koleksiyonlar söz konusu olduğunda bu özellikle zordur.

Bu, değişmez koleksiyonların devreye girdiği yerdir.

Bu koleksiyonlar ImmutableHashSet <T> ve ImmutableList <T> .

Verim

Değişmez koleksiyonlar yapısal paylaşımı sağlamak için altındaki ağaç veri yapılarını kullandıklarından, performans özellikleri değişebilir koleksiyonlardan farklıdır. Kilitlenebilir değiştirilebilir bir koleksiyonla karşılaştırıldığında, sonuçlar kilit çekişmesine ve erişim modellerine bağlı olacaktır. Ancak, değişmez koleksiyonlar hakkında başka bir blog gönderisinden alınmıştır :

S: Değişmez koleksiyonların yavaş olduğunu duydum. Bunlar farklı mı? Performans veya bellek önemli olduğunda kullanabilir miyim?

C: Bu değişmez koleksiyonlar, bellek paylaşımını dengelerken değişebilir koleksiyonlar için rekabetçi performans özelliklerine sahip olacak şekilde ayarlanmıştır. Bazı durumlarda, değiştirilebilir algoritmalar hem algoritmik hem de gerçek zamanlı olarak çok hızlıdır, bazen daha da hızlıdır, diğer durumlarda ise algoritmik olarak daha karmaşıktır. Ancak çoğu durumda fark göz ardı edilebilir. Genel olarak işi yapmak için en basit kodu kullanmalı ve ardından performansı gerektiği gibi ayarlamalısınız. Değişmez koleksiyonlar, özellikle iş parçacığı güvenliği dikkate alındığında basit kod yazmanıza yardımcı olur.

Diğer bir deyişle, birçok durumda fark göze çarpmayacak ve daha basit bir seçim ile gitmelisiniz - eşzamanlı setler için kullanmak olacaktır ImmutableHashSet<T>, çünkü mevcut bir kilitleme değiştirilebilir uygulamanız yok! :-)


1
ImmutableHashSet<T>amacınız paylaşılan durumu birden çok iş parçacığından güncellemekse veya burada bir şey kaçırıyor muyum?
tugberk

7
@tugberk Evet ve hayır. Kümenin değişmez olduğu için, koleksiyonun kendisinin size yardım etmediği referansı güncellemeniz gerekir. İyi haber, paylaşılan bir veri yapısını birden çok iş parçacığından güncelleştirme karmaşık sorununu, paylaşılan bir referansı güncelleme konusunda çok daha basit bir soruna indirmiş olmanızdır. Kütüphane, bu konuda size yardımcı olması için ImmutableInterlocked.Update yöntemini sunar.
Søren Boisen

1
@ SørenBoisenjust, değişmez koleksiyonları okudu ve bunları nasıl güvenli bir şekilde kullanacağını anlamaya çalıştı. ImmutableInterlocked.Updateeksik bağlantı gibi görünüyor. Teşekkür ederim!
xneg

4

ISet<T>Eşzamanlı hale getirmenin zor kısmı , ayarlanan yöntemlerin (birleşim, kavşak, fark) doğada yinelemeli olmasıdır. En azından, her iki seti kilitlerken, operasyona dahil olan setlerden birinin tüm n üyelerini tekrarlamanız gerekir.

ConcurrentDictionary<T,byte>Yineleme sırasında tüm seti kilitlemeniz gerektiğinde a'nın avantajlarını kaybedersiniz . Kilitleme olmadan, bu işlemler diş için güvenli değildir.

Ek yükü göz önüne alındığında, ConcurrentDictionary<T,byte>muhtemelen daha hafif ağırlığı kullanmak HashSet<T>ve her şeyi kilitlerde sarmak akıllıca olacaktır .

Ayarlanan işlemlere ihtiyacınız yoksa, tuş eklerken değer olarak kullanın ConcurrentDictionary<T,byte>ve default(byte)kullanın.


2

Komple çözümleri tercih ederim, bu yüzden bunu yaptım: Sayımın farklı bir şekilde uygulandığını düşünün çünkü değerlerini saymaya çalışırken neden hasheti okumak için yasaklanması gerektiğini anlamıyorum.

@Zen, Başladığınız için teşekkürler.

[DebuggerDisplay("Count = {Count}")]
[Serializable]
public class ConcurrentHashSet<T> : ICollection<T>, ISet<T>, ISerializable, IDeserializationCallback
{
    private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

    private readonly HashSet<T> _hashSet = new HashSet<T>();

    public ConcurrentHashSet()
    {
    }

    public ConcurrentHashSet(IEqualityComparer<T> comparer)
    {
        _hashSet = new HashSet<T>(comparer);
    }

    public ConcurrentHashSet(IEnumerable<T> collection)
    {
        _hashSet = new HashSet<T>(collection);
    }

    public ConcurrentHashSet(IEnumerable<T> collection, IEqualityComparer<T> comparer)
    {
        _hashSet = new HashSet<T>(collection, comparer);
    }

    protected ConcurrentHashSet(SerializationInfo info, StreamingContext context)
    {
        _hashSet = new HashSet<T>();

        // not sure about this one really...
        var iSerializable = _hashSet as ISerializable;
        iSerializable.GetObjectData(info, context);
    }

    #region Dispose

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

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
            if (_lock != null)
                _lock.Dispose();
    }

    public IEnumerator<T> GetEnumerator()
    {
        return _hashSet.GetEnumerator();
    }

    ~ConcurrentHashSet()
    {
        Dispose(false);
    }

    public void OnDeserialization(object sender)
    {
        _hashSet.OnDeserialization(sender);
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        _hashSet.GetObjectData(info, context);
    }

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

    #endregion

    public void Add(T item)
    {
        _lock.EnterWriteLock();
        try
        {
            _hashSet.Add(item);
        }
        finally
        {
            if(_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public void UnionWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        _lock.EnterReadLock();
        try
        {
            _hashSet.UnionWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            if (_lock.IsReadLockHeld) _lock.ExitReadLock();
        }
    }

    public void IntersectWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        _lock.EnterReadLock();
        try
        {
            _hashSet.IntersectWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            if (_lock.IsReadLockHeld) _lock.ExitReadLock();
        }
    }

    public void ExceptWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        _lock.EnterReadLock();
        try
        {
            _hashSet.ExceptWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            if (_lock.IsReadLockHeld) _lock.ExitReadLock();
        }
    }

    public void SymmetricExceptWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            _hashSet.SymmetricExceptWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsSubsetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsSubsetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsSupersetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsSupersetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsProperSupersetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsProperSupersetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsProperSubsetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsProperSubsetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool Overlaps(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.Overlaps(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool SetEquals(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.SetEquals(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    bool ISet<T>.Add(T item)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.Add(item);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public void Clear()
    {
        _lock.EnterWriteLock();
        try
        {
            _hashSet.Clear();
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool Contains(T item)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.Contains(item);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        _lock.EnterWriteLock();
        try
        {
            _hashSet.CopyTo(array, arrayIndex);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool Remove(T item)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.Remove(item);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public int Count
    {
        get
        {
            _lock.EnterWriteLock();
            try
            {
                return _hashSet.Count;
            }
            finally
            {
                if(_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            }

        }
    }

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

Kilit atılır ... ama hafızası ne zaman serbest bırakılır?
David Rettenbacher

1
@Warappa çöp toplama üzerine çıktı. Tek şey i el ile null şeyler ve bir sınıf içindeki tüm varlıklarını temizlemek zaman konular içerir ve böylece MAY sızıntı bellek (ObservableCollection ve onun değişti olayı kullanmak gibi). Konuyla ilgili anlayışım hakkında bilgi ekleyebilirseniz önerilere açığım. Ben de çöp toplama üzerinde araştırma birkaç gün geçirdim ve her zaman yeni bilgi merak ediyorum
Dbl

@ AndreasMüller iyi cevap, ancak neden '_lock.EnterWriteLock ();' ardından '_lock.EnterReadLock ();' 'IntersectWith' gibi bazı yöntemlerde, varsayılan olarak girildiğinde yazma kilidi herhangi bir okuma (lar) önleyeceği için burada okuma görünümüne gerek olmadığını düşünüyorum.
Jalal Said

Her zaman mecbur kalırsan EnterWriteLock, neden var EnterReadLockbile? Okuma kilidi gibi yöntemler için kullanılamaz Containsmı?
ErikE

2
Bu bir ConcurrentHashSet değil, bir ThreadSafeHashSet. Kendi kendine uygulama ile ilgili @ZenLulz cevabı hakkındaki yorumuma bakın. Bu uygulamaları kullanan herkesin uygulamalarında ciddi bir hata olacağından% 99 eminim.
George Mavritsakis
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.