.NET Frameworkの同時HashSet <T>?


151

次のクラスがあります。

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

直接現場に行き、複数のスレッドによる同時アクセスから保護するためのより良い解決策はありますか?


どのように下のコレクションの一つを使用する方法についてSystem.Collections.Concurrent
I4V

8
もちろん、非公開にします。
ハンスパッサント

3
並行性の観点からは、Dataフィールドが公開されていることを除いて、あなたが行ったことに多くの間違いはないと思います!懸念がある場合は、ReaderWriterLockSlimを使用して読み取りパフォーマンスを向上させることができます。 msdn.microsoft.com/en-us/library/...
アラン・エルダー

@AllanElder ReaderWriterLockは、複数のリーダーと単一のライターの場合に役立ちます(効率的)。これがOPのケースであるかどうかを知る必要があります
Sriram Sakthivel '20 / 09/20

2
現在の実装は実際には「同時」ではありません:)スレッドセーフです。
2013

回答:


164

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

ConcurrentDictionary(推奨)

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

private ConcurrentDictionary<string, byte> _data;

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

ソース:ソーシャルMSDN

ConcurrentBag

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

private ConcurrentBag<string> _data;

自己実装

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

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

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

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

編集:try例外をスローし、finallyブロックに含まれる命令を実行する可能性があるため、ブロックの外にある入り口ロックメソッドを移動します。


8
ジャンク値のある辞書はリストです
Ralf

44
@ラルフまあ、それは順序付けされていないので、リストではなくセットです。
サービー2013

11
「コレクションと同期(スレッドセーフティ)」に関するMSDNのかなり短いドキュメントによると、System.Collectionsのクラスと関連する名前空間は、複数のスレッドで安全に読み取ることができます。つまり、HashSetは複数のスレッドで安全に読み取ることができます。
Hank Schultz

7
@Oliver、参照は、たとえそれがnull参照であっても、エントリごとにはるかに多くのメモリを使用します(参照には、32ビットランタイムでは4バイト、64ビットランタイムでは8バイトが必要です)。したがってbyte、、空の構造体などを使用すると、メモリフットプリントが削減される場合があります(または、ランタイムがネイティブメモリ境界でデータを調整してアクセスを高速化する場合はそうでない場合があります)。
Lucero 2014年

4
自己実装は、ConcurrentHashSetではなく、ThreadSafeHashSetです。これら2つの間に大きな違いがあり、それがMicorosftがSynchronizedCollectionsを放棄した理由です(人々はそれを誤解しました)。GetOrAddなどの「並行」操作を行うには(ディクショナリのように)実装する必要があります。そうしないと、追加のロックがなければ並行性を保証できません。しかし、クラスの外で追加のロックが必要な場合は、最初から単純なHashSetを使用しないのはなぜですか。
George Mavritsakis

36

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

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

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

concurrentHashSet.TryRemove("foo");

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

出力:2

ここからNuGetから入手して、GitHub ソースを確認できます


3
これは、認められた答えであり、優れた実装です
にやにや笑い作者'20年

Addの名前をTryAddに変更してConcurrentDictionaryと一貫性を持たせる必要はありませんか?
Neo

8
@Neoいいえ...意図的にHashSet <T>セマンティクスを使用しているため、Addを呼び出し、アイテムが追加されたか(true)、またはアイテムがすでに存在していたか(false)を示すブール値を返します。 msdn.microsoft.com/en-us/library/bb353005(v=vs.110).aspx
G-Mac

それはISet<T>実際にHashSet<T>セマンティクスと一致するインターフェースboを実装すべきではないでしょうか?
ネクロマンサー2018

1
@Nekromancer答えで述べたように、これらのセットメソッドを並行実装で提供することは意味がないと思います。 Overlapsたとえば、実行中にインスタンスをロックするか、すでに間違っている可能性のある回答を提供する必要があります。どちらのオプションもIMOには適していません(ユーザーが外部から追加することもできます)。
i3arnon

21

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

Microsoft不変コレクション

背後にあるMSチームのブログ投稿から:

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

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

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

ここで不変のコレクションが登場します。

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

パフォーマンス

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

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

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

言い換えると、多くの場合、違いは顕著ではなく、より単純な選択を使用するImmutableHashSet<T>必要があります。既存のロック変更可能な実装がないため、並行セットにはを使用することになります。:-)


1
ImmutableHashSet<T>あなたの意図が複数のスレッドから共有状態を更新することである場合、または私がここで何か不足している場合、あまり役に立ちませんか?
tugberk

7
@tugberkはい、いいえ。セットは不変なので、コレクションへの参照を更新する必要がありますが、コレクション自体は役に立ちません。良いニュースは、共有データ構造を更新するという複雑な問題を複数のスレッドから共有参照を更新するというはるかに簡単な問題に減らしたことです。ライブラリは、ImmutableInterlocked.Updateメソッドを提供して、それを支援します。
セーレンBoisen

1
@SørenBoisenjustは不変コレクションについて読み、それらをスレッドセーフで使用する方法を理解しようとしました。ImmutableInterlocked.Updateミッシングリンクのようです。ありがとうございました!
xneg 2018

4

ISet<T>コンカレントを作成する際の注意が必要な部分は、セットメソッド(ユニオン、インターセクション、差異)が本質的に反復的であることです。少なくとも、両方のセットをロックしながら、操作に含まれる1つのセットのすべてのnメンバーを反復処理する必要があります。

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

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

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


2

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

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

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

ロックは破棄されます...しかし、内部のハッシュセットはどうですか、いつメモリが解放されますか?
David Rettenbacher 2014年

1
@Warappaガベージコレクション時に解放されます。手動で物事をnullにしてクラス内の存在をすべてクリアする唯一の時間は、サブジェクトにイベントが含まれているため、メモリをリークする可能性があります(ObservableCollectionとその変更されたイベントを使用する場合など)。この件に関する私の理解に知識を追加していただければ、提案をお待ちしています。私もガベージコレクションの調査に数日を費やしており、常に新しい情報に興味があります
Dbl

@AndreasMüller良い答えですが、なぜ '_lock.EnterWriteLock();'に続いて '_lock.EnterReadLock();'を使用しているのかと思います。'IntersectWith'のようないくつかのメソッドでは、書き込みロックはデフォルトで入力されたときに読み取りを防止するため、ここでの読み取りの外観は必要ないと思います。
Jalalは

あなたがいつもしなければならないならEnterWriteLock、なぜEnterReadLock存在するのですか?読み取りロックは次のような方法では使用できませんContainsか?
ErikE 2015年

2
これはConcurrentHashSetではなく、ThreadSafeHashSetです。自己実装に関する@ZenLulzの回答に関する私のコメントを参照してください。これらの実装を使用したすべてのユーザーが、アプリケーションに重大なバグを抱えていることを99%確信しています。
George Mavritsakis
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.