.NET Framework에서 동시 HashSet <T>?


151

다음 수업이 있습니다.

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

다른 스레드에서 "데이터"필드를 변경해야하므로 현재 스레드 안전 구현에 대한 의견이 필요합니다.

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
물론 비공개로 만드십시오.
Hans Passant

3
동시성 관점에서 데이터 필드 이외의 다른 작업을 공개적으로 잘못 생각하지는 않습니다. 걱정이되는 경우 ReaderWriterLockSlim을 사용하면 더 나은 읽기 성능을 얻을 수 있습니다. msdn.microsoft.com/ko-kr/library/…
Allan Elder

@AllanElder ReaderWriterLock는 여러 독자와 한 명의 작가 일 때 유용합니다. 우리는 이것이 OP인지의 여부를 알고있다
Sriram Sakthivel

2
현재 구현은 실제로 '동시'가 아닙니다 :) 스레드 안전합니다.
undefined

답변:


164

구현이 정확합니다. 불행히도 .NET Framework는 내장 동시 해시 셋 유형을 제공하지 않습니다. 그러나 몇 가지 해결 방법이 있습니다.

동시 사전 (권장)

첫 번째 ConcurrentDictionary<TKey, TValue>는 네임 스페이스 에서 클래스를 사용하는 것 System.Collections.Concurrent입니다. 이 경우 값이 의미가 없으므로 단순 byte(메모리에서 1 바이트)을 사용할 수 있습니다 .

private ConcurrentDictionary<string, byte> _data;

유형은 스레드로부터 안전 HashSet<T>하고 키와 값이 다른 객체와 동일한 이점을 제공하기 때문에 권장되는 옵션 입니다.

출처 : 소셜 MSDN

동시 가방

중복 항목이 마음에 들지 않으면 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블록에 포함 된 명령을 실행할 수 있으므로 블록을 ouside하십시오 .


8
정크 값을 가진 사전은 목록입니다
Ralf

44
@Ralf 글쎄, 그것은 순서가 없기 때문에 목록이 아니라 세트입니다.
Servy

11
"Collections and Synchronization (Thread Safety)" 에 대한 MSDN의 짧은 문서에 따르면 System.Collections 및 관련 네임 스페이스의 클래스를 여러 스레드에서 안전하게 읽을 수 있습니다. 이것은 여러 스레드가 HashSet을 안전하게 읽을 수 있음을 의미합니다.
행크 슐츠

7
@Oliver, 참조는 참조 인 경우에도 항목 당 훨씬 더 많은 메모리를 사용합니다 (참조는 null32 비트 런타임에서 4 바이트, 64 비트 런타임에서 8 바이트 필요). 따라서, byte빈 구조체 또는 이와 유사한 것을 사용하면 메모리 풋 프린트가 줄어들 수 있습니다 (또는 런타임이 빠른 액세스를 위해 런타임이 데이터를 기본 메모리 경계에 정렬하는 경우에는 그렇지 않을 수 있음).
Lucero

4
Self-implementation은 ConcurrentHashSet이 아니라 ThreadSafeHashSet입니다. 그 둘 사이에는 큰 차이가 있으며 이것이 Micorosft가 SynchronizedCollections를 포기한 이유입니다 (사람들이 잘못했습니다). GetOrAdd 등과 같은 "동시"작업을 수행하려면 사전과 같이 구현해야합니다. 그렇지 않으면 추가 잠금없이 동시성을 보장 할 수 없습니다. 그러나 클래스 외부에서 추가 잠금이 필요한 경우 처음부터 간단한 HashSet을 사용하지 않는 이유는 무엇입니까?
George Mavritsakis

36

를 감싸 ConcurrentDictionary거나 잠그는 대신 HashSet실제 ConcurrentHashSet기반으로 만들었습니다 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
이것은 허용 대답, 큰 구현해야한다
smirkingman

추가 로 이름을 바꿀 수 TryAdd 가 함께 일관된 수 있도록 ConcurrentDictionary ?
Neo

8
@Neo No ... 의도적으로 HashSet <T> 의미를 사용하기 때문에 Add 를 호출 하고 항목이 추가되었는지 (true) 또는 이미 존재하는지 (false)를 나타내는 부울을 반환합니다. msdn.microsoft.com/ko-kr/library/bb353005(v=vs.110).aspx
G-Mac

ISet<T>인터페이스 bo가 실제로 HashSet<T>의미 와 일치 하는지 구현해서는 안 됩니까?
Nekromancer

1
@Nekromancer는 대답에서 말했듯이 이러한 구현 방법을 동시 구현으로 제공하는 것이 의미가 없다고 생각합니다. Overlaps예를 들어, 실행 중에 인스턴스를 잠 그거나 이미 잘못된 답변을 제공해야합니다. 두 옵션 모두 잘못된 IMO입니다 (소비자가 외부에서 추가 할 수 있음).
i3arnon

21

다른 사람이 언급하지 않았으므로 귀하의 특정 목적에 적합하거나 적합하지 않은 대체 방법을 제공 할 것입니다.

Microsoft 불변 컬렉션

MS 팀 의 블로그 게시물 에서 :

동시 생성 및 실행이 그 어느 때보 다 쉬워 지지만 여전히 근본적인 문제 중 하나 인 가변 공유 상태가 여전히 존재합니다. 여러 스레드에서 읽는 것은 일반적으로 매우 쉽지만 일단 상태를 업데이트해야하는 경우 특히 잠금이 필요한 설계에서는 훨씬 어려워집니다.

잠금의 대안은 불변 상태를 이용하는 것입니다. 불변의 데이터 구조는 절대 변경되지 않으므로 다른 사람의 발가락을 밟을 염려없이 다른 스레드간에 자유롭게 전달할 수 있습니다.

이 디자인은 새로운 문제를 만듭니다. 매번 전체 상태를 복사하지 않고 상태 변경을 어떻게 관리합니까? 컬렉션이 관련된 경우 특히 까다 롭습니다.

불변의 컬렉션이 들어온 곳입니다.

이러한 컬렉션에는 ImmutableHashSet <T>ImmutableList <T>가 포함 됩니다.

공연

변경 불가능한 콜렉션은 아래의 트리 데이터 구조를 사용하여 구조 공유를 가능하게하기 때문에 성능 특성이 변경 가능한 콜렉션과 다릅니다. 잠금 변경 가능 콜렉션과 비교할 때 결과는 잠금 경합 및 액세스 패턴에 따라 다릅니다. 그러나 불변 컬렉션에 대한 다른 블로그 게시물 에서 가져온 것 입니다.

Q : 불변의 컬렉션이 느리다고 들었습니다. 이것들은 다른가요? 성능이나 메모리가 중요한 경우 사용할 수 있습니까?

A :이 불변 컬렉션은 메모리 공유의 균형을 유지하면서 변경 가능한 컬렉션에 대해 경쟁적인 성능 특성을 갖도록 크게 조정되었습니다. 어떤 경우에는 알고리즘 및 실제 시간 모두에서 가변 컬렉션만큼 빠르며 때로는 더 빠르며 다른 경우에는 알고리즘 적으로 더 복잡합니다. 그러나 많은 경우에 그 차이는 무시할 수 있습니다. 일반적으로 가장 간단한 코드를 사용하여 작업을 완료 한 다음 필요에 따라 성능을 조정해야합니다. 불변 컬렉션은 특히 스레드 안전성을 고려해야하는 경우 간단한 코드를 작성하는 데 도움이됩니다.

다시 말해서, 많은 경우에 차이는 눈에 띄지 ImmutableHashSet<T>않으며 기존 잠금 변경 가능 구현이 없기 때문에 동시 세트의 경우 더 간단한 선택을 사용해야 합니다! :-)


1
ImmutableHashSet<T>여러 스레드에서 공유 상태를 업데이트하려는 의도가 많거나 여기에 뭔가 빠졌습니까?
tugberk

7
@tugberk 예, 아니오 세트는 변경할 수 없으므로 컬렉션 자체가 도움이되지 않는 참조를 업데이트해야합니다. 좋은 소식은 공유 데이터 구조를 업데이트하는 복잡한 문제를 여러 스레드에서 공유 참조 업데이트의 훨씬 간단한 문제로 줄였다는 것입니다. 라이브러리는이를 지원하는 ImmutableInterlocked.Update 메소드를 제공합니다.
Søren Boisen

1
@ SørenBoisenjust는 불변 컬렉션에 대해 읽고 스레드 안전을 사용하는 방법을 알아 내려고 노력했습니다. ImmutableInterlocked.Update링크가 누락 된 것 같습니다. 감사합니다!
xneg

4

ISet<T>동시성을 만드는 데있어 까다로운 부분은 설정된 메소드 (연합, 교차점, 차이)가 본질적으로 반복적이라는 것입니다. 최소한 두 세트를 잠그는 동안 조작에 포함 된 세트 중 하나의 n 개 멤버 전체를 반복해야합니다.

ConcurrentDictionary<T,byte>반복하는 동안 전체 세트를 잠 가야 하는 경우 에는 이점이 없습니다 . 잠금이 없으면 이러한 작업은 스레드 안전이 아닙니다.

의 추가 오버 헤드가 주어지면 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

1
@Warappa는 가비지 수집시 릴리스됩니다. 내가 수동으로 물건을 null로 분류하고 클래스 내에서 전체 존재를 지우는 유일한 시간은 주제에 이벤트가 포함되어 메모리가 누출 될 수있는 경우입니다 (ObservableCollection 및 변경된 이벤트를 사용할 때와 같이). 주제에 대한 나의 이해에 지식을 추가 할 수 있다면 제안을 할 수 있습니다. 가비지 수집에 대해서도 며칠을 보냈으며 항상 새로운 정보에 대해 궁금합니다.
Dbl

@ AndreasMüller 좋은 대답이지만 왜 '_lock.EnterWriteLock ();'다음에 '_lock.EnterReadLock ();'을 사용하는지 궁금합니다. 'IntersectWith'와 같은 일부 방법에서는 쓰기 잠금이 기본적으로 입력 할 때 판독을 방해하므로 여기에서 읽기 모양이 필요하지 않다고 생각합니다.
Jalal이

항상해야한다면 EnterWriteLockEnterReadLock존재 하는가? 읽기 잠금을 다음과 같은 방법으로 사용할 수 Contains없습니까?
ErikE

2
이것은 ConcurrentHashSet이 아니라 ThreadSafeHashSet입니다. 자체 구현에 관한 @ZenLulz 답변에 대한 내 의견을 참조하십시오. 99 %는 해당 구현을 사용한 모든 사람이 응용 프로그램에 심각한 버그가있을 것이라고 확신합니다.
George Mavritsakis
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.