.Net 4.0에 ConcurrentList <T>가 없습니까?


198

System.Collections.Concurrent.Net 4.0에서 새로운 네임 스페이스 를 보게되어 매우 기뻤습니다 . 내가 본 것 ConcurrentDictionary, ConcurrentQueue, ConcurrentStack, ConcurrentBagBlockingCollection.

의문의 여지없이 누락 된 것 중 하나는 ConcurrentList<T>입니다. 직접 작성해야합니까 (또는 웹에서 가져와야합니까)?

여기에 명백한 것이 빠져 있습니까?



4
@RodrigoReis, ConcurrentBag <T>는 순서가없는 컬렉션이고 List <T>는 순서가 있습니다.
Adam Calvet Bohl

4
멀티 스레드 환경에서 어떻게 정렬 된 컬렉션을 가질 수 있습니까? 의도적으로 요소 순서를 제어 할 수는 없습니다.
Jeremy Holovacs

대신 잠금 사용
에릭 Bergstedt

닷넷 소스 코드에는 ThreadSafeList.cs라는 파일이 있는데 아래 코드와 같이 많이 보입니다. ReaderWriterLockSlim도 사용하고 왜 간단한 lock (obj) 대신에 그것을 사용하는지 알아 내려고 노력하고 있습니까?
콜린 라마레

답변:


166

나는 그것을 다시 시도했다 (또한 GitHub에 ). 구현에는 몇 가지 문제가 있었으므로 여기에 들어 가지 않습니다. 더 중요한 것은 내가 배운 것을 말해 드리겠습니다.

첫째, IList<T>잠금 및 스레드 안전을 완전히 구현할 방법이 없습니다 . 특히 O (1) 임의 액세스를 잊어 버리지 않는 한 (즉, "속임수를 사용하고 링크 된 목록을 사용하여 인덱싱을 빨지 않으면) 무작위 삽입 및 제거가 작동 하지 않습니다 .

내가 생각 가치가있을 수있는 것은의 스레드 안전, 제한 집합했다 IList<T>: 특히, 허용 할 하나의 Add랜덤 제공하지 않습니다 읽기 전용 인덱스의 액세스를 (하지만 Insert, RemoveAt등, 또한 및 랜덤 쓰기 액세스).

이것이 저의 ConcurrentList<T>구현 목표였습니다 . 그러나 다중 스레드 시나리오에서 성능을 테스트했을 때 단순히 추가를 동기화하는 List<T>것이 더 빠르다 는 것을 알았습니다 . 기본적으로에 추가하는 List<T>것은 이미 번개처럼 빠릅니다. 관련된 계산 단계의 복잡성은 아주 적습니다 (인덱스를 증가시키고 배열의 요소에 할당하는 것이 실제입니다 ). 당신은 필요 이의 잠금 경합의 어떤 종류를 볼 동시 쓰기를, 그럼에도 불구하고 각 쓰기의 평균 성능은에서 비록 잠금이없는 구현이지만 여전히 더 비쌉니다 ConcurrentList<T>.

목록의 내부 배열 자체 크기를 조정해야하는 경우는 드물지만 적은 비용을 지불합니다. 그래서 결국 나는이가하다고 결론 하나 추가 전용 틈새 시나리오 ConcurrentList<T>수집 유형이 나을 : 당신이 원하는 때 보장 에 요소를 추가하는 낮은 오버 헤드 매일 전화를 (그래서, 상각 성능 목표에 반대).

생각만큼 유용한 수업이 아닙니다.


52
또한 List<T>오래된 skool, 모니터 기반 동기화를 사용하는 것과 비슷한 것이 필요한 경우 SynchronizedCollection<T>BCL에 숨겨져 있습니다. msdn.microsoft.com/en-us/library/ms668265.aspx
LukeH

8
하나의 작은 추가 : 용량 생성기 매개 변수를 사용하여 크기 조정 시나리오를 피하십시오 (가능한 한).
Henk Holterman

2
ConcurrentList이길 수있는 가장 큰 시나리오 는 목록에 추가 할 활동이 많지 않지만 동시 독자가 많은 경우입니다. 독자의 오버 헤드를 단일 메모리 장벽으로 줄일 수있다 (그리고 독자들이 약간의 데이터에 대해 신경 쓰지 않았다면 제거 할 수있다).
supercat

2
@ 케빈 (Kevin) : ConcurrentList<T>독자들이 잠금을 필요로하지 않고 일정한 오버 헤드를 추가하면서 일관성있는 상태를 볼 수 있도록하는 방식 으로 구성하는 것은 매우 사소한 일입니다. 목록이 크기 32에서 64로 확장되면 size-32 배열을 유지하고 새로운 size-64 배열을 만듭니다. 다음 32 개 항목을 각각 추가 할 때 새 배열의 32-63 개 슬롯에 넣고 이전 항목을 size-32 배열에서 새 항목으로 복사하십시오. 64 번째 항목이 추가 될 때까지 독자는 크기 32 배열에서 항목 0-31을 찾고 크기 64 배열에서 항목 32-63을 찾습니다.
supercat

2
64 번째 항목이 추가되면 size-32 배열은 여전히 ​​항목 0-31을 가져 오기 위해 작동하지만 독자는 더 이상 사용할 필요가 없습니다. 모든 항목 0-63에는 size-64 배열을, 항목 64-127에는 size-128 배열을 사용할 수 있습니다. 사용할 두 어레이 중 하나를 선택하는 오버 헤드와 원하는 경우 메모리 장벽은 상상할 수있는 가장 효율적인 리더 라이터 잠금의 오버 헤드보다 적습니다. 쓰기는 아마도 잠금을 사용해야 할 것입니다 (특히 삽입 할 때마다 새로운 객체 인스턴스를 생성하는 것을 신경 쓰지 않지만 잠금이 저렴해야하는 경우 잠금이 없을 수 있습니다)
supercat

38

ConcurrentList를 무엇에 사용 하시겠습니까?

스레드 세계에서 랜덤 액세스 컨테이너의 개념은 보이는 것처럼 유용하지 않습니다. 진술

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

전체적으로 여전히 스레드 안전하지 않습니다.

ConcurrentList를 작성하는 대신 존재하는 솔루션을 작성하십시오. 가장 일반적인 클래스는 ConcurrentBag, 특히 BlockingCollection입니다.


좋은 지적. 아직도 내가하고있는 일은 조금 더 평범한 것입니다. ConcurrentBag <T>를 IList <T>에 할당하려고합니다. 내 속성을 IEnumerable <T>로 전환 할 수는 있지만 물건을 추가 할 수는 없습니다.
Alan

1
@ Alan : 목록을 잠그지 않고 구현할 방법이 없습니다. Monitor어쨌든 이미 그렇게 할 수 있기 때문에 동시 목록에 대한 이유가 없습니다.
Billy ONeal

6
@dcp-예 이것은 본질적으로 스레드 안전하지 않습니다. ConcurrentDictionary에는 AddOrUpdate, GetOrAdd, TryUpdate 등과 같은 하나의 원자 연산에서이를 수행하는 특수 메소드가 있습니다. 때로는 사전을 수정하지 않고 키가 있는지 알고 싶어하기 때문에 ContainsKey를 가지고 있습니다 (HashSet 생각)
Zarat

3
@dcp-ContainsKey는 자체적으로 스레드 안전합니다. (예 : ContainsKey!가 아닌) 예제는 첫 번째 결정에 따라 두 번째 호출을 수행하기 때문에 경쟁 조건이 있습니다.이 시점에서 이미 오래되었을 수 있습니다.
Zarat

2
k, 동의하지 않습니다. 매우 유용한 시나리오가 있다고 생각합니다. 작업자 스레드 쓰기는 UI 스레드를 읽고 인터페이스를 적절하게 업데이트합니다. 정렬 된 방식으로 항목을 추가하려면 랜덤 액세스 쓰기가 필요합니다. 데이터에 스택과 뷰를 사용할 수도 있지만 2 개의 콜렉션을 유지해야합니다
Eric Ouellet

19

이미 제공 된 큰 답변과 관련하여 스레드 안전 IList를 원할 때가 있습니다. 진보하거나 공상하는 것은 없습니다. 많은 경우 성능이 중요하지만 때로는 그다지 걱정하지 않는 경우가 있습니다. 예, "TryGetValue"와 같은 메소드가없는 경우 항상 문제가 발생하지만 대부분의 경우 모든 것을 잠그는 것에 대해 걱정할 필요없이 열거 할 수있는 것을 원합니다. 그리고 그렇습니다. 누군가 내 구현에서 교착 상태 또는 무언가로 이어질 수있는 "버그"를 찾을 수는 있지만 (정직하게 생각할 수 있습니다) 멀티 스레딩과 관련하여 코드를 올바르게 작성하지 않으면 어쨌든 교착 상태가 될 것입니다. 이를 염두에두고 이러한 기본적인 요구를 제공하는 간단한 ConcurrentList 구현을 만들기로 결정했습니다.

그리고 그 가치에 대해 : 나는 정규 List와 ConcurrentList에 10,000,000 개의 항목을 추가하는 기본 테스트를 수행했으며 그 결과는 다음과 같습니다.

완료된 목록 : 7793 밀리 초 동시 완료 : 8064 밀리 초

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
좋아, 오래된 대답이지만 여전히 : RemoveAt(int index)스레드 안전하지 않으며 Insert(int index, T item)index == 0에만 안전합니다. 반환 IndexOf()은 즉시 구식입니다.에 대해 시작조차하지 마십시오 this[int].
Henk Holterman

2
그리고 ~ Finalizer ()가 필요하지 않습니다.
Henk Holterman

2
교착 상태의 가능성을 방지하기 위해 포기했다고 말하면서 동시에 사용하여 단일 ReaderWriterLockSlim교착 상태를 쉽게 만들 수 있습니다 EnterUpgradeableReadLock(). 그러나 그것을 사용하지 않고 외부에서 액세스 할 수있는 잠금을 만들지 않으며 읽기 잠금을 유지하면서 쓰기 잠금을 시작하는 메서드를 호출하지 않으므로 클래스를 사용하더라도 교착 상태가 더 이상 발생하지 않습니다 아마도.
Eugene Beresovsky

1
비 동시 인터페이스는 동시 액세스에 적합하지 않습니다. 예를 들어 다음은 원자가 아닙니다 var l = new ConcurrentList<string>(); /* ... */ l[0] += "asdf";. 일반적으로 읽기 / 쓰기 콤보는 동시에 수행 할 때 심각한 문제를 일으킬 수 있습니다. 동시 데이터 구조는 일반적으로 그와 같은 방법을 제공하는 이유는 ConcurrentDictionary'S AddOrGet등 NB 내 상수 (상기 부재가 이미 같은 밑줄 표시되어 있으므로 중복) 반복 this.클러.
Eugene Beresovsky

1
감사합니다 유진 "this"를 넣은 .NET Reflector를 많이 사용하고 있습니다. 모든 비 정적 필드에서. 따라서 저는 같은 것을 선호하도록 성장했습니다. 이 비 동시 인터페이스가 적절하지 않은 경우 : 구현에 대해 여러 작업을 수행하려고하면 신뢰할 수 없게 될 수 있습니다. 그러나 여기서 요구 사항은 단순히 컬렉션을 손상시키지 않고 단일 작업 (추가, 제거, 지우기 또는 열거)을 수행 할 수 있다는 것입니다. 기본적으로 모든 것을 둘러싼 잠금 문을 넣을 필요가 없습니다.
브라이언 부스

11

ConcurrentList(연결된 목록이 아닌 크기 조정 가능한 배열로) 비 블로킹 작업으로는 쓰기가 쉽지 않습니다. API는 "동시"버전으로 잘 변환되지 않습니다.


12
쓰기가 어려울뿐만 아니라 유용한 인터페이스를 파악하기도 어렵습니다.
코드 InChaos

11

ConcurrentList가없는 이유는 기본적으로 작성할 수 없기 때문입니다. 그 이유는 IList의 여러 중요한 작업이 인덱스에 의존하기 때문에 평범한 것이 작동하지 않기 때문입니다. 예를 들면 다음과 같습니다.

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

저자가 겪고있는 결과는 "고양이"앞에 "개"를 삽입하는 것이지만 멀티 스레드 환경에서는 두 줄의 코드 사이에 어떤 일이 발생할 수 있습니다. 예를 들어, 다른 스레드가 수행 list.RemoveAt(0)하여 전체 목록을 왼쪽으로 이동시킬 수 있지만 catIndex 는 변경되지 않습니다. 여기서의 영향은 Insert작업이 실제로 "개" 고양이 뒤가 아니라 고양이 것입니다.

이 질문에 대한 "답변"으로 제공되는 몇 가지 구현은 의미가 있지만, 위와 같이 신뢰할 수있는 결과를 제공하지는 않습니다. 다중 스레드 환경에서 목록과 같은 의미를 실제로 원한다면 목록 구현 메소드 안에 잠금을 넣어서 얻을 수 없습니다 . 사용하는 모든 인덱스가 잠금 컨텍스트 내에 완전히 존재하는지 확인해야합니다. 결론은 올바른 잠금으로 다중 스레드 환경에서 목록을 사용할 수 있지만 목록 자체는 해당 세계에 존재할 수 없다는 것입니다.

동시 목록이 필요하다고 생각되면 실제로 두 가지 가능성이 있습니다.

  1. 실제로 필요한 것은 ConcurrentBag입니다
  2. List와 자체 동시성 컨트롤로 구현 된 고유 한 컬렉션을 만들어야합니다.

ConcurrentBag가 있고 IList로 전달해야하는 위치에있는 경우 호출하는 메소드에서 고양이와 함께 위와 같은 작업을 수행하도록 지정했기 때문에 문제가 있습니다. 개. 대부분의 세계에서 의미하는 것은 호출하는 메소드가 단순히 다중 스레드 환경에서 작동하도록 제작되지 않았다는 것입니다. 즉, 리팩토링하여 리팩토링하거나 할 수 없으면 매우 신중하게 처리해야합니다. 거의 확실하게 자체 잠금을 사용하여 자신 만의 컬렉션을 만들고 잠금 내에서 문제를 일으키는 메서드를 호출해야합니다.


5

경우에 쓰기 크게 능가 읽거나 (그러나 빈번) 글은 비 동시되어 A, 복사 (copy-on-write) 접근이 적절할 수있다.

아래에 표시된 구현은

  • 자물쇠가없는
  • 시간이 오래 걸리더라도 동시 수정 작업이 진행되는 동안에도 동시 읽기 속도가 엄청나게 빠릅니다.
  • "스냅 샷"은 변경할 수 없기 때문에 잠금없는 원 자성 이 가능합니다. 즉 var snap = _list; snap[snap.Count - 1];(물론 빈 목록을 제외하고는) 던지지 않을 것입니다. 또한 스냅 샷 의미론을 사용하여 스레드 안전 열거를 무료로 얻을 수 있습니다. 어떻게 불변성을 좋아합니다!
  • 일반적으로 구현 되며 모든 데이터 구조모든 유형의 수정에 적용 가능
  • 죽은 간단한 , 즉 테스트, 디버그, 코드를 판독하여 확인하기 쉬운
  • .Net 3.5에서 사용 가능

COW (Copy-On-Write)가 작동하려면 데이터 구조를 효과적으로 변경할 수없는 상태로 유지해야합니다 . 즉, 다른 스레드에서 사용할 수있게 한 후에는 데이터 구조 를 변경할 수 없습니다. 수정하고 싶을 때

  1. 구조를 복제
  2. 클론을 수정하다
  3. 수정 된 클론에 대한 참조에서 원자 적으로 교환

암호

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

용법

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

더 많은 성능이 필요한 경우 메소드를 강화하는 데 도움이됩니다. 예를 들어 원하는 모든 수정 유형 (추가, 제거 등)에 대해 하나의 메소드를 작성하고 함수 포인터 cloner및 하드 코드를 작성하십시오 op.

NB # 1 아무도 불변의 데이터 구조를 수정하지 않도록하는 것은 귀하의 책임입니다. 이를 막기 위해 일반적인 구현 에서는 할 수있는 일은 없지만 List<T>,를 전문화 할 때 List.AsReadOnly ()를 사용하여 수정을 막을 수 있습니다.

NB # 2 목록의 값에주의하십시오. 위의 copy on write 접근법은 목록 멤버쉽 만 보호하지만 문자열을 넣지 않고 다른 가변 객체를 넣을 경우 스레드 안전을 관리해야합니다 (예 : 잠금). 그러나 이것은이 솔루션과 직교하며 변경 가능한 값의 잠금은 문제없이 쉽게 사용할 수 있습니다. 당신은 그것을 알고 있어야합니다.

NB # 3 데이터 구조가 거대하고 자주 수정하는 경우, 복사 중 복사 방식은 메모리 소비 및 관련 복사 비용과 관련하여 금지 될 수 있습니다. 이 경우 대신 MS의 Immutable Collection 을 사용할 수 있습니다 .


3

System.Collections.Generic.List<t>이미 여러 독자에게 스레드로부터 안전합니다. 여러 작성자가 스레드를 안전하게 만들려고하는 것은 의미가 없습니다. (Henk와 Stephen이 이미 언급 한 이유로)


목록에 5 개의 스레드가 추가되는 시나리오를 볼 수 없습니까? 이렇게하면 목록이 모두 종료되기 전에도 레코드가 누적되는 것을 볼 수 있습니다.
Alan

9
@Alan-ConcurrentQueue, ConcurrentStack 또는 ConcurrentBag입니다. ConcurrentList를 이해하려면 사용 가능한 클래스가 충분하지 않은 유스 케이스를 제공해야합니다. 인덱스의 요소가 동시 제거를 통해 무작위로 변경 될 수있는 경우 색인 액세스를 원하는 이유를 모르겠습니다. "잠금"읽기의 경우 이미 기존 동시 클래스의 스냅 샷을 작성하여 목록에 넣을 수 있습니다.
Zarat

당신 말이 맞아요-인덱스 액세스를 원하지 않습니다. 나는 일반적으로 IList <T>를 IEnumerable의 프록시로 사용하여 .Add (T) 새 요소를 사용할 수 있습니다. 그것이 바로 질문의 근원입니다.
Alan

@Alan : 그런 다음 목록이 아닌 대기열을 원합니다.
Billy ONeal

3
당신이 틀렸다고 생각합니다. 말하기 : 여러 독자에게 안전하다고해서 동시에 쓸 수는 없습니다. 쓰기도 삭제를 의미하며 반복하는 동안 삭제하면 오류가 발생합니다.
Eric Ouellet

2

어떤 사람들은 일부 상품 포인트와 일부 내 생각을 강조했습니다.

  • 무작위 접근 자 (인덱서)를 사용할 수없는 것처럼 보이지만 나에게는 잘 보입니다. 멀티 스레드 컬렉션에는 인덱서 및 삭제와 같이 실패 할 수있는 많은 방법이 있다고 생각하면됩니다. "실패"또는 단순히 "끝에 추가"와 같은 쓰기 접근 자에 대한 실패 (대체) 조치를 정의 할 수도 있습니다.
  • 다중 스레드 컨텍스트이기 때문에 항상 다중 스레드 컨텍스트에서 사용되는 것은 아닙니다. 또는 한 명의 작가와 한 명의 독자 만 사용할 수도 있습니다.
  • 안전한 방법으로 인덱서를 사용할 수있는 또 다른 방법은 루트를 사용하여 조치를 콜렉션의 잠금으로 랩핑하는 것입니다 (공개 된 경우).
  • 많은 사람들에게 rootLock을 보이게 만드는 것은 "좋은 습관"입니다. 이 점이 100 % 확실하지 않다면 숨겨져 있으면 사용자에게 많은 유연성을 제거 할 수 있기 때문입니다. 우리는 항상 멀티 스레드 프로그래밍은 누구에게도 해당되지 않는다는 것을 기억해야합니다. 모든 종류의 잘못된 사용을 막을 수는 없습니다.
  • Microsoft는 멀티 스레드 모음의 올바른 사용법을 소개하기 위해 몇 가지 작업을 수행하고 새로운 표준을 정의해야합니다. 먼저 IEnumerator에는 moveNext가 없어야하지만 true 또는 false를 반환하고 T 유형의 출력 매개 변수를 가져 오는 GetNext가 있어야합니다 (이렇게하면 반복이 더 이상 차단되지 않음). 또한 Microsoft는 이미 foreach에서 "사용"을 내부적으로 사용하고 있지만 "사용"(컬렉션 뷰의 버그 및 더 많은 위치에서)으로 랩핑하지 않고 IEnumerator를 직접 사용하기도합니다. 이 버그는 안전한 반복자에 대한 좋은 가능성을 제거합니다 ... 생성자에서 컬렉션을 잠그고 Dispose 메소드에서 잠금을 해제하는 반복자-foreach 메소드를 차단합니다.

그것은 답이 아닙니다. 이것은 특정 장소에 실제로 맞지 않는 의견 일뿐입니다.

... 필자의 결론, Microsoft는 MultiThreaded 컬렉션을보다 쉽게 ​​사용할 수 있도록 "foreach"를 약간 변경해야합니다. 또한 IEnumerator 사용 규칙을 따라야합니다. 그때까지는 차단 반복자를 사용하지만 "IList"를 따르지 않는 MultiThreadList를 쉽게 작성할 수 있습니다. 대신 "삽입", "제거"및 임의 접근 자 (인덱서)에서 예외없이 실패 할 수있는 자체 "IListPersonnal"인터페이스를 정의해야합니다. 그러나 표준이 아닌 경우 누가 사용하고 싶습니까?


ConcurrentOrderedBag<T>의 읽기 전용 구현을 포함 하는를 쉽게 작성할 수 는 IList<T>있지만 완전 스레드 안전 int Add(T value)메소드를 제공합니다. 왜 ForEach변경이 필요한지 모르겠습니다 . 비록 Microsoft가 명시 적으로 그렇게 말하지는 않지만, 그들의 관행은 IEnumerator<T>그것이 만들어 졌을 때 존재했던 컬렉션 내용을 열거 하는 것이 완벽하게 수용 가능하다는 것을 제안합니다 . 컬렉션 수정 예외는 열거자가 글리치없는 작동을 보장 할 수없는 경우에만 필요합니다.
supercat

MT 컬렉션을 반복하면 디자인 방식이 예외로 이어질 수 있습니다. 모든 예외를 포착 하시겠습니까? 내 책에서 예외는 예외이며 정상적인 코드 실행에서 발생해서는 안됩니다. 그렇지 않으면 예외를 방지하기 위해 동시성으로 인해 예외가 발생하지 않도록 컬렉션을 잠 그거나 복사본을 안전한 방법 (예 : 잠금)으로 가져 오거나 컬렉션에서 매우 복잡한 메커니즘을 구현해야합니다. 내 경우마다 발생하는 동안 컬렉션을 잠그고 관련 코드를 추가하는 IEnumeratorMT를 추가하는 것이 좋을 것입니다 ...
Eric Ouellet

발생할 수있는 또 다른 사항은 반복자를 얻을 때 컬렉션을 잠글 수 있고 반복자가 GC 수집되면 컬렉션을 잠금 해제 할 수 있다는 것입니다. Microsfot에 따르면 그들은 이미 IEnumerable이 IDisposable인지 확인하고 ForEach의 끝에서 GC를 호출합니다. 주요 문제는 GC를 호출하지 않고 다른 곳에서도 IEnumerable을 사용한다는 것입니다. IEnumerable 활성화 잠금을위한 새로운 명확한 MT 인터페이스를 사용하면 문제의 적어도 일부를 해결할 수 있습니다. (사람들이 전화하지 않는 것을 막을 수는 없습니다).
Eric Ouellet

공개 GetEnumerator메소드가 콜렉션을 리턴 한 후 잠그는 것은 매우 나쁜 형식입니다 . 이러한 설계는 교착 상태로 쉽게 이어질 수 있습니다. 이 IEnumerable<T>열거는 컬렉션이 변경 되더라도 완료 할 것으로 예상 될 수 있는지의 어떠한 표시도 제공하지 않는다; 최선의 방법은 자신의 메서드를 작성하여 그렇게하는 것입니다 . 스레드 안전 열거를 지원하는 IEnumerable<T>경우에만 스레드로부터 안전하다는 사실을 문서화하는 방법이 있습니다 IEnumerable<T>.
supercat

IEnumerable<T>return type에 "Snapshot"메소드가 포함 되어 있으면 가장 도움이 될 것 IEnumerable<T>입니다. 불변의 컬렉션은 스스로를 돌려 줄 수 있습니다. 바운드 컬렉션 아무것도 그 밖에 자신을 복사 할 수 없었다 경우 List<T>또는 T[]및 전화 GetEnumerator그합니다. 무제한 컬렉션은 구현할 Snapshot수 있으며, 목록으로 내용을 채우지 않으면 예외를 던질 수 없는 컬렉션이 구현 될 수 있습니다.
supercat

1

순차적으로 코드를 실행함에있어서, 사용 된 데이터 구조는 (잘 쓰여진) 동시에 실행되는 코드와 다르다. 그 이유는 순차적 코드가 암시 적 순서를 의미하기 때문입니다. 그러나 동시 코드는 순서를 의미하지 않습니다. 더 나은 아직 정의 된 순서의 부족을 의미합니다!

이로 인해 List와 같은 순서가 내재 된 데이터 구조는 동시 문제를 해결하는 데별로 유용하지 않습니다. 목록은 순서를 의미하지만 해당 순서가 무엇인지 명확하게 정의하지는 않습니다. 이 때문에 목록을 조작하는 코드의 실행 순서는 목록의 암시 적 순서를 어느 정도 결정하여 효율적인 동시 솔루션과 직접 충돌합니다.

동시성은 코드 문제가 아니라 데이터 문제라는 것을 기억하십시오! 코드를 먼저 구현하거나 기존 순차 코드를 다시 작성할 수 없으며 잘 설계된 동시 솔루션을 얻을 수 없습니다. 암시 적 순서는 동시 시스템에는 존재하지 않음을 명심하면서 데이터 구조를 먼저 디자인해야합니다.


1

잠금없는 복사 및 쓰기 접근 방식은 너무 많은 항목을 처리하지 않는 경우 효과적입니다. 내가 쓴 수업은 다음과 같습니다.

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

}

사용법 예 : orders_BUY = CopyAndWriteList.Clear (orders_BUY);


잠금 대신 목록의 사본을 작성하고 목록을 수정하고 참조를 새 목록으로 설정합니다. 따라서 반복되는 다른 스레드는 문제를 일으키지 않습니다.
Rob The Quant

0

Brian 과 비슷한 것을 구현했습니다 . 광산은 다릅니다.

  • 어레이를 직접 관리합니다.
  • try 블록 내에 잠금을 입력하지 않습니다.
  • yield return열거자를 생성하는 데 사용 합니다.
  • 잠금 재귀를 지원합니다. 이를 통해 반복 중에 목록에서 읽을 수 있습니다.
  • 가능한 경우 업그레이드 가능한 읽기 잠금을 사용합니다.
  • DoSyncGetSync목록에 독점적으로 액세스해야하는 순차적 인 상호 작용을 허용하는 방법.

코드 :

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

두 개의 스레드가 try블록 시작 Remove또는 인덱서 세터 의 시작 부분에 동시에 들어가면 어떻게됩니까 ?
제임스

불가능한 것 같은 @James. msdn.microsoft.com/en-us/library/… 에서주의 사항을 읽으십시오 . 이 코드를 실행하면, 당신은 잠금에게 2 시간을 입력하지 않을 것이다 : gist.github.com/ronnieoverby/59b715c3676127a113c3
로니 오버 비

@Ronny Overby : 흥미 롭습니다. 그 점을 감안할 때 업그레이드 가능한 읽기 잠금과 쓰기 잠금 사이의 시간에 유일한 작업이 수행 된 모든 기능에서 UpgradableReadLock을 제거하면 훨씬 더 나은 성능을 발휘할 것으로 생각됩니다. 쓰기 잠금 내에서 해당 검사를 수행하는 것이 범위를 벗어난 지 확인하는 것보다 검사가 더 잘 수행 될 수 있습니다.
James

이 클래스는 오프셋 기반 함수 (대부분의 함수)는 외부 잠금 방식이 없다면 실제로 안전하게 사용할 수 없기 때문에 매우 유용하지 않습니다. 실제로 얻을 때와 무언가를 얻을 수 있습니다.
제임스

1
IList동시 시나리오에서 시맨틱 의 유용성 이 최대로 제한 된다는 것을 알고 있다는 기록을 남기고 싶었습니다 . 나는 그 실현에 오기 전에 아마이 코드를 썼다. 내 경험은 받아 들여진 대답의 작가와 같습니다. 동기화 및 IList <T>에 대해 알고있는 것을 시도해 보았으며 그렇게함으로써 무언가를 배웠습니다.
Ronnie Overby
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.