System.Collections.Concurrent
.Net 4.0에서 새로운 네임 스페이스 를 보게되어 매우 기뻤습니다 . 내가 본 것 ConcurrentDictionary
, ConcurrentQueue
, ConcurrentStack
, ConcurrentBag
와 BlockingCollection
.
의문의 여지없이 누락 된 것 중 하나는 ConcurrentList<T>
입니다. 직접 작성해야합니까 (또는 웹에서 가져와야합니까)?
여기에 명백한 것이 빠져 있습니까?
답변
나는 그것을 다시 시도했다 (또한 GitHub에 ). 구현에는 몇 가지 문제가 있었으므로 여기에 들어 가지 않습니다. 더 중요한 것은 내가 배운 것을 말해 드리겠습니다.
첫째, IList<T>
잠금 및 스레드 안전을 완전히 구현할 방법이 없습니다 . 특히 O (1) 임의 액세스를 잊어 버리지 않는 한 (즉, “속임수를 사용하고 링크 된 목록을 사용하여 인덱싱을 빨지 않으면) 무작위 삽입 및 제거가 작동 하지 않습니다 .
내가 생각 가치가있을 수있는 것은의 스레드 안전, 제한 집합했다 IList<T>
: 특히, 허용 할 하나의 Add
랜덤 제공하지 않습니다 읽기 전용 인덱스의 액세스를 (하지만 Insert
, RemoveAt
등, 또한 및 랜덤 쓰기 액세스).
이것이 저의 ConcurrentList<T>
구현 목표였습니다 . 그러나 다중 스레드 시나리오에서 성능을 테스트했을 때 단순히 추가를 동기화하는 List<T>
것이 더 빠르다 는 것을 알았습니다 . 기본적으로에 추가하는 List<T>
것은 이미 번개처럼 빠릅니다. 관련된 계산 단계의 복잡성은 아주 적습니다 (인덱스를 증가시키고 배열의 요소에 할당하는 것이 실제입니다 ). 당신은 필요 톤 이의 잠금 경합의 어떤 종류를 볼 동시 쓰기를, 그럼에도 불구하고 각 쓰기의 평균 성능은에서 비록 잠금이없는 구현이지만 여전히 더 비쌉니다 ConcurrentList<T>
.
목록의 내부 배열 자체 크기를 조정해야하는 경우는 드물지만 적은 비용을 지불합니다. 그래서 결국 나는이가하다고 결론 하나 추가 전용 틈새 시나리오 ConcurrentList<T>
수집 유형이 나을 : 당신이 원하는 때 보장 에 요소를 추가하는 낮은 오버 헤드 매일 전화를 (그래서, 상각 성능 목표에 반대).
생각만큼 유용한 수업이 아닙니다.
답변
ConcurrentList를 무엇에 사용 하시겠습니까?
스레드 세계에서 랜덤 액세스 컨테이너의 개념은 보이는 것처럼 유용하지 않습니다. 진술
if (i < MyConcurrentList.Count)
x = MyConcurrentList[i];
전체적으로 여전히 스레드 안전하지 않습니다.
ConcurrentList를 작성하는 대신 존재하는 솔루션을 작성하십시오. 가장 일반적인 클래스는 ConcurrentBag, 특히 BlockingCollection입니다.
답변
이미 제공 된 큰 답변과 관련하여 스레드 안전 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
}
답변
ConcurrentList
(연결된 목록이 아닌 크기 조정 가능한 배열로) 비 블로킹 작업으로는 쓰기가 쉽지 않습니다. API는 “동시”버전으로 잘 변환되지 않습니다.
답변
ConcurrentList가없는 이유는 기본적으로 작성할 수 없기 때문입니다. 그 이유는 IList의 여러 중요한 작업이 인덱스에 의존하기 때문에 평범한 것이 작동하지 않기 때문입니다. 예를 들면 다음과 같습니다.
int catIndex = list.IndexOf("cat");
list.Insert(catIndex, "dog");
저자가 겪고있는 결과는 “고양이”앞에 “개”를 삽입하는 것이지만 멀티 스레드 환경에서는 두 줄의 코드 사이에 어떤 일이 발생할 수 있습니다. 예를 들어, 다른 스레드가 수행 list.RemoveAt(0)
하여 전체 목록을 왼쪽으로 이동시킬 수 있지만 catIndex 는 변경되지 않습니다. 여기서의 영향은 Insert
작업이 실제로 “개” 를 고양이 뒤가 아니라 고양이 것입니다.
이 질문에 대한 “답변”으로 제공되는 몇 가지 구현은 의미가 있지만, 위와 같이 신뢰할 수있는 결과를 제공하지는 않습니다. 다중 스레드 환경에서 목록과 같은 의미를 실제로 원한다면 목록 구현 메소드 안에 잠금을 넣어서 얻을 수 없습니다 . 사용하는 모든 인덱스가 잠금 컨텍스트 내에 완전히 존재하는지 확인해야합니다. 결론은 올바른 잠금으로 다중 스레드 환경에서 목록을 사용할 수 있지만 목록 자체는 해당 세계에 존재할 수 없다는 것입니다.
동시 목록이 필요하다고 생각되면 실제로 두 가지 가능성이 있습니다.
- 실제로 필요한 것은 ConcurrentBag입니다
- List와 자체 동시성 컨트롤로 구현 된 고유 한 컬렉션을 만들어야합니다.
ConcurrentBag가 있고 IList로 전달해야하는 위치에있는 경우 호출하는 메소드에서 고양이와 함께 위와 같은 작업을 수행하도록 지정했기 때문에 문제가 있습니다. 개. 대부분의 세계에서 의미하는 것은 호출하는 메소드가 단순히 다중 스레드 환경에서 작동하도록 제작되지 않았다는 것입니다. 즉, 리팩토링하여 리팩토링하거나 할 수 없으면 매우 신중하게 처리해야합니다. 거의 확실하게 자체 잠금을 사용하여 자신 만의 컬렉션을 만들고 잠금 내에서 문제를 일으키는 메서드를 호출해야합니다.
답변
경우에 쓰기 크게 능가 읽거나 (그러나 빈번) 글은 비 동시되어 A, 복사 (copy-on-write) 접근이 적절할 수있다.
아래에 표시된 구현은
- 자물쇠가없는
- 시간이 오래 걸리더라도 동시 수정 작업이 진행되는 동안에도 동시 읽기 속도가 엄청나게 빠릅니다.
- “스냅 샷”은 변경할 수 없기 때문에 잠금없는 원 자성 이 가능합니다. 즉
var snap = _list; snap[snap.Count - 1];
(물론 빈 목록을 제외하고는) 던지지 않을 것입니다. 또한 스냅 샷 의미론을 사용하여 스레드 안전 열거를 무료로 얻을 수 있습니다. 어떻게 불변성을 좋아합니다! - 일반적으로 구현 되며 모든 데이터 구조 및 모든 유형의 수정에 적용 가능
- 죽은 간단한 , 즉 테스트, 디버그, 코드를 판독하여 확인하기 쉬운
- .Net 3.5에서 사용 가능
COW (Copy-On-Write)가 작동하려면 데이터 구조를 효과적으로 변경할 수없는 상태로 유지해야합니다 . 즉, 다른 스레드에서 사용할 수있게 한 후에는 데이터 구조 를 변경할 수 없습니다. 수정하고 싶을 때
- 구조를 복제
- 클론을 수정하다
- 수정 된 클론에 대한 참조에서 원자 적으로 교환
암호
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 을 사용할 수 있습니다 .
답변
System.Collections.Generic.List<t>
이미 여러 독자에게 스레드로부터 안전합니다. 여러 작성자가 스레드를 안전하게 만들려고하는 것은 의미가 없습니다. (Henk와 Stephen이 이미 언급 한 이유로)