web-dev-qa-db-ja.com

.NET Frameworkでの同時HashSet <T>?

次のクラスがあります。

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

フィールド「Data」を異なるスレッドから変更する必要があるので、現在のスレッドセーフな実装について意見をお願いします。

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

フィールドに直接アクセスし、複数のスレッドによる同時アクセスから保護するためのより良いソリューションはありますか?

129
kukab

実装は正しいです。 .NET Frameworkは、残念ながら組み込みの同時ハッシュセットタイプを提供しません。ただし、いくつかの回避策があります。

ConcurrentDictionary(推奨)

最初の方法は、名前空間ConcurrentDictionary<TKey, TValue>でクラスSystem.Collections.Concurrentを使用することです。この場合、値は無意味なので、単純なbyte(メモリ内の1バイト)を使用できます。

private ConcurrentDictionary<string, byte> _data;

タイプはスレッドセーフであり、キーと値が異なるオブジェクトであることを除き、HashSet<T>と同じ利点を提供するため、これは推奨オプションです。

ソース: Social MSDN

ConcurrentBag

重複したエントリを気にしない場合は、前のクラスと同じ名前空間でクラスConcurrentBag<T>を使用できます。

private ConcurrentBag<string> _data;

自己実装

最後に、あなたがしたように、ロックまたは.NETがスレッドセーフになるように提供する他の方法を使用して、独自のデータ型を実装できます。ここに素晴らしい例があります: 。NetでConcurrentHashSetを実装する方法

このソリューションの唯一の欠点は、読み取り操作であっても、タイプHashSet<T>が公式に同時アクセスしないことです。

リンクされた投稿のコードを引用します(元は Ben Mosher によって作成されました)。

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

EDIT:tryブロックを除外する入り口ロックメソッドを移動します。例外をスローし、finallyブロック。

143
ZenLulz

ConcurrentDictionaryをラップまたはHashSetにロックする代わりに、ConcurrentHashSetに基づいて実際のConcurrentDictionaryを作成しました。

この実装は、同時シナリオIMOであまり意味をなさないため、HashSetの設定操作なしでアイテムごとの基本操作をサポートします。

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

concurrentHashSet.TryRemove("foo");

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

出力:2

NuGet here から取得でき、GitHub here でソースを確認できます。

28
i3arnon

他の誰もそれを言及しなかったので、私はあなたの特定の目的に適切かもしれないし、そうでないかもしれない代替アプローチを提供します:

Microsoft不変コレクション

ブログ投稿 からMSチームの背後で:

並行して作成および実行することはかつてないほど簡単ですが、根本的な問題の1つは依然として存在します。それは、可変共有状態です。通常、複数のスレッドからの読み取りは非常に簡単ですが、状態の更新が必要になると、特にロックを必要とする設計では、状態がはるかに難しくなります。

ロックの代わりに、不変の状態を利用します。不変のデータ構造は決して変更されないことが保証されているため、他の人のつま先を踏むことを心配することなく、異なるスレッド間で自由に渡すことができます。

ただし、この設計では新しい問題が発生します。毎回状態全体をコピーせずに状態の変更をどのように管理しますか?コレクションが関係する場合、これは特に注意が必要です。

これが不変コレクションの出番です。

これらのコレクションには、 ImmutableHashSet <T> および ImmutableList <T> が含まれます。

性能

不変コレクションは、その下でツリーデータ構造を使用して構造共有を可能にするため、パフォーマンス特性は可変コレクションとは異なります。ロック可能な可変コレクションと比較する場合、結果はロックの競合とアクセスパターンに依存します。ただし、 別のブログ投稿 から、不変のコレクションについて:

Q:不変のコレクションは遅いと聞きました。これらは違いますか?パフォーマンスやメモリが重要なときに使用できますか?

A:これらの不変コレクションは、メモリ共有のバランスを取りながら、可変コレクションと競合するパフォーマンス特性を持つように高度に調整されています。場合によっては、アルゴリズムと実時間の両方で可変コレクションとほぼ同じ速度で、時にはさらに高速になりますが、アルゴリズム的にはより複雑になります。ただし、多くの場合、違いはごくわずかです。通常、最も簡単なコードを使用してジョブを完了し、必要に応じてパフォーマンスを調整する必要があります。不変コレクションは、特にスレッドセーフを考慮する必要がある場合に、簡単なコードを書くのに役立ちます。

つまり、多くの場合、違いは目立たないため、既存のロック可能な実装がないため、より簡単な選択(同時セットではImmutableHashSet<T>を使用すること)を選択する必要があります。 :-)

19
Søren Boisen

私は完全なソリューションを好むので、これを行いました:値をカウントしようとしているときにハッシュセットを読み取ることを禁止する理由がわからないので、私のカウントは別の方法で実装されています。

@Zen、始めてくれてありがとう。

[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; }
    }
}
3
Dbl

ISet<T>を並行にすることに関する注意すべき点は、集合メソッド(結合、交差、差)が本質的に反復的であることです。少なくとも、両方のセットをロックしながら、操作に関係するセットのいずれかのnメンバーすべてを反復処理する必要があります。

反復中にセット全体をロックする必要がある場合、ConcurrentDictionary<T,byte>の利点を失います。ロックしないと、これらの操作はスレッドセーフではありません。

ConcurrentDictionary<T,byte>の追加オーバーヘッドを考えると、おそらくより軽いHashSet<T>を使用し、すべてをロックで囲むだけの方が賢明でしょう。

セット操作が必要ない場合は、ConcurrentDictionary<T,byte>を使用し、キーを追加するときにdefault(byte)を値として使用します。

3
pugby