.NET-사전 잠금과 동시 사전


131

ConcurrentDictionary유형 에 대한 충분한 정보를 찾을 수 없으므로 여기에 대해 물어볼 것이라고 생각했습니다.

현재 a Dictionary를 사용하여 여러 스레드 (스레드 풀에서 정확한 양의 스레드가 아닌)로 지속적으로 액세스하는 모든 사용자를 보유하고 있으며 액세스를 동기화했습니다.

최근에 .NET 4.0에 스레드 안전 컬렉션이 있다는 것을 알았으며 매우 기쁘게 생각합니다. Dictionary동기화 된 액세스 가있는 일반 옵션을 사용 하거나 ConcurrentDictionary이미 스레드로부터 안전한 옵션을 사용할 수 있기 때문에 '보다 효율적이고 관리하기 쉬운'옵션이 무엇인지 궁금 합니다.

.NET 4.0에 대한 참조 ConcurrentDictionary


답변:


146

스레드 세이프 콜렉션과 비스 레드 세이프 콜렉션은 다른 방식으로 볼 수 있습니다.

계산대를 제외하고 점원이없는 상점을 고려하십시오. 사람들이 책임감있게 행동하지 않으면 많은 문제가 있습니다. 예를 들어, 고객이 피라미드 캔에서 캔을 가져 가고 있지만 점원이 현재 피라미드를 짓고 있다면 모든 것이 망가질 것입니다. 또는 두 명의 고객이 동시에 같은 상품을 구매하면 누가 이기는가? 싸움이 있을까요? 이것은 스레드로부터 안전하지 않은 콜렉션입니다. 문제를 피할 수있는 방법은 많이 있지만, 모두 일종의 잠금 또는 어떤 방식 으로든 명시적인 액세스가 필요합니다.

반면에 책상에 점원이있는 상점을 생각해보십시오. 상점을 통해서만 쇼핑 할 수 있습니다. 당신은 줄을 서서 그에게 물건을 요구하고, 그는 그것을 당신에게 가져오고, 당신은 줄을 벗어납니다. 여러 개의 물품이 필요한 경우, 각 왕복에서 기억할 수있는만큼만 물품을 수령 할 수 있지만, 점원의 호그를 피하지 않도록주의해야합니다. 이렇게하면 다른 고객이 뒤에서 줄을 서게됩니다.

이제 이것을 고려하십시오. 점원 한 명이있는 상점에서 줄 앞에 줄을 서서 점원에게 "화장지가 있습니까?"라고 물으면 "예"라고 말한 다음 "좋아요." "내가 얼마나 필요한지 알면 다시 연락 할 것입니다."그런 다음 줄 앞에서 돌아올 때마다 매장을 매진 할 수 있습니다. 이 시나리오는 스레드 세이프 컬렉션에 의해 방지되지 않습니다.

스레드 세이프 컬렉션은 내부 데이터 구조가 여러 스레드에서 액세스 된 경우에도 항상 유효한지 확인합니다.

스레드로부터 안전하지 않은 콜렉션에는 그러한 보장이 제공되지 않습니다. 예를 들어, 하나의 스레드에서 이진 트리에 무언가를 추가하고 다른 스레드가 트리를 재조정하는 데 바쁘지만 항목이 추가되거나 그 후에도 여전히 트리가 유효하다는 보장은 없습니다. 희망을 넘어서 손상 될 수 있습니다.

그러나 스레드 세이프 컬렉션은 스레드에 대한 순차적 작업이 모두 내부 데이터 구조의 동일한 "스냅 샷"에서 작동한다는 것을 보증하지 않습니다. 이는 다음과 같은 코드가있는 경우를 의미합니다.

if (tree.Count > 0)
    Debug.WriteLine(tree.First().ToString());

당신은 inbetween 때문에 NullReferenceException이를 얻을 수 있습니다 tree.Counttree.First()다른 스레드 수단이 나무의 나머지 노드 없애 게했다, First()반환됩니다 null.

이 시나리오의 경우 해당 컬렉션에 원하는 것을 얻을 수있는 안전한 방법이 있는지 확인해야합니다. 위의 코드를 다시 작성해야하거나 잠 가야 할 수도 있습니다.


9
이 기사를 읽을 때마다, 비록 그것이 사실이지만, 어떻게 든 우리는 잘못된 길을 가고 있습니다.
scope_creep

19
스레드 사용 응용 프로그램의 주요 문제는 변경 가능한 데이터 구조입니다. 당신이 그것을 피할 수 있다면, 당신은 문제의 힙을 저장합니다.
Lasse V. Karlsen

89
이것은 스레드 안전성에 대한 좋은 설명이지만 도움이 될 수는 없지만 실제로 OP의 질문을 해결하지 못한 것처럼 느낍니다. OP가 요청한 것 (그리고 나중에이 질문을 찾은 것)은 표준을 사용하고 Dictionary직접 잠금을 처리하는 ConcurrentDictionary것과 .NET 4 +에 내장 된 유형을 사용하는 것의 차이점이었습니다 . 나는 실제로 이것이 받아 들여 졌다는 약간 당황했다.
mclark1129

4
스레드 안전성은 올바른 컬렉션을 사용하는 것 이상입니다. 올바른 컬렉션을 사용하는 것이 시작일 뿐이며 처리해야 할 유일한 것은 아닙니다. OP가 알고 싶어했던 것 같습니다. 왜 이것이 받아 들여 졌는지 짐작할 수 없습니다.이 글을 쓴 지 오래되었습니다.
Lasse V. Karlsen

2
@ LasseV.Karlsen이 말하려고하는 것은 스레드 안전이 달성하기 쉽지만 그 안전을 처리하는 방법에 일정한 수준의 동시성을 제공해야한다고 생각합니다. "객체를 스레드로부터 안전하게 유지하면서 동시에 더 많은 작업을 수행 할 수 있습니까?" 이 예를 고려하십시오. 두 고객이 다른 품목을 반품하려고합니다. 관리자로서 귀하의 상점 재입고 여행을 할 때, 한 번의 여행으로 두 품목을 모두 재입고하고 두 고객의 요청을 모두 처리 할 수 ​​있습니까? 아니면 고객이 품목을 반품 할 때마다 여행을 하시겠습니까?
Alexandru

68

thread-safe가 쓰레드 문제를 무시할 수 있다는 의미는 아니기 때문에 thread-safe 컬렉션을 사용할 때는 여전히주의해야합니다. 컬렉션이 스레드로부터 안전하다고 광고하면 일반적으로 여러 스레드가 동시에 읽고 쓰는 경우에도 일관성있는 상태로 유지됩니다. 그러나 이것이 단일 스레드가 여러 메소드를 호출하는 경우 "논리적"결과 시퀀스를 보게된다는 의미는 아닙니다.

예를 들어, 먼저 키가 있는지 확인한 다음 나중에 해당 키에 해당하는 값을 얻는 경우 다른 스레드가 키를 제거했을 수 있기 때문에 해당 키가 더 이상 ConcurrentDictionary 버전을 사용하더라도 존재하지 않을 수 있습니다. 이 경우에도 여전히 잠금을 사용해야합니다 (또는 더 나은 방법 : TryGetValue 를 사용하여 두 호출을 결합 ).

따라서 사용하지만 동시성 문제를 모두 무시할 수있는 무료 패스를 제공한다고 생각하지 마십시오. 여전히 조심해야합니다.


4
따라서 기본적으로 객체를 올바르게 사용하지 않으면 작동하지 않는다고 말하는 것입니까?
ChaosPandion 2009

3
기본적으로 일반적인 Contains-> Add 대신 TryAdd를 사용해야합니다.
ChaosPandion 2009

3
@Bruno : 아니요. 잠그지 않은 경우 다른 스레드가 두 호출 사이의 키를 제거했을 수 있습니다.
Mark Byers

13
여기에서 소리를내는 것을 의미하지는 않지만 TryGetValue항상 일부 Dictionary이고 항상 권장되는 접근 방식이었습니다. 소개하는 중요한 "동시"방법 ConcurrentDictionaryAddOrUpdateGetOrAdd입니다. 따라서 좋은 대답이지만 더 좋은 예를 선택할 수 있습니다.
Aaronaught 2009

4
트랜잭션이있는 데이터베이스와 달리 대부분의 동시 메모리 내 검색 구조는 격리 개념을 놓치 므로 내부 데이터 구조가 올바른지 확인합니다.
Chang

39

내부적으로 ConcurrentDictionary는 각 해시 버킷에 대해 별도의 잠금을 사용합니다. 단일 항목에서 작동하는 Add / TryGetValue 및 이와 유사한 메소드 만 사용하는 한, 사전은 각각의 달콤한 성능 이점이있는 거의 잠금이없는 데이터 구조로 작동합니다. 열거 방법 (Count 속성 포함)은 모든 버킷을 한 번에 잠그므로 성능 측면에서 동기화 된 사전보다 더 나쁩니다.

나는 ConcurrentDictionary를 사용한다고 말하고 싶습니다.


15

ConcurrentDictionary.GetOrAdd 메서드는 대부분의 멀티 스레드 시나리오에 필요한 것입니다.


1
TryGet도 사용합니다. 그렇지 않으면 키가 이미 존재할 때마다 두 번째 매개 변수를 GetOrAdd로 초기화하는 노력을 낭비하게됩니다.
Jorrit Schippers

7
GetOrAdd 에는 오버로드 GetOrAdd (TKey, Func <TKey, TValue>) 가 있으므로 사전에 키가없는 경우에만 두 번째 매개 변수를 지연 초기화 할 수 있습니다. 또한 TryGetValue 는 수정하지 않고 데이터를 가져 오는 데 사용됩니다. 이러한 각 메소드에 대한 후속 호출로 인해 다른 스레드가 두 호출 사이에 키를 추가하거나 제거했을 수 있습니다.
sgnsajgon

Lasse의 게시물이 우수 하더라도이 질문에 대한 답변이 표시되어야합니다.
Spivonious

13

.Net 3.5sp1 용 Reactive Extensions 를 보셨습니까? Jon Skeet에 따르면 이들은 .Net3.5 sp1에 대한 병렬 확장 및 동시 데이터 구조 번들을 백 포트했습니다.

.Net 4 Beta 2에 대한 샘플 세트가 있는데, 이는 병렬 확장을 사용하는 방법에 대해 아주 자세하게 설명합니다.

방금 지난 주에 32 개의 스레드를 사용하여 I / O를 수행하는 ConcurrentDictionary를 테스트했습니다. 그것은 광고 된대로 작동하는 것 같습니다. 엄청난 양의 테스트가 이루어 졌음을 나타냅니다.

편집 : .NET 4 동시 사전 및 패턴.

Microsoft는 Patterns of Paralell Programming이라는 PDF를 출시했습니다. .Net 4 Concurrent 확장에 사용하기에 적합한 패턴과 피해야 할 안티 패턴에 대해 자세히 설명 된대로 다운로드 할 가치가 있습니다.여기있어.


4

기본적으로 새로운 ConcurrentDictionary와 함께 가고 싶습니다. 스레드로부터 안전한 프로그램을 만들려면 코드를 적게 작성해야합니다.


Concurrent Dictionery를 계속 진행하면 문제가 있습니까?
kbvishnu

1

ConcurrentDictionary그것을 충족하는 경우 최고의 선택이 될 것입니다 모든 스레드 안전 요구 사항을. 그렇지 않은 경우, 다시 말해서 약간 복잡한 작업을 수행하는 경우 일반 Dictionary+ lock가 더 나은 옵션 일 수 있습니다. 예를 들어 사전에 일부 주문을 추가하고 있으며 총 주문량을 계속 업데이트하려고한다고 가정합니다. 다음과 같은 코드를 작성할 수 있습니다.

private ConcurrentDictionary<int, Order> _dict;
private Decimal _totalAmount = 0;

while (await enumerator.MoveNextAsync())
{
    Order order = enumerator.Current;
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

이 코드는 스레드 안전하지 않습니다. _totalAmount필드를 업데이트하는 여러 스레드가 필드를 손상된 상태로 둘 수 있습니다. 따라서 다음과 같이 보호하려고 할 수 있습니다 lock.

_dict.TryAdd(order.Id, order);
lock (_locker) _totalAmount += order.Amount;

이 코드는 "safer"이지만 여전히 스레드 안전하지 않습니다. _totalAmount사전의 항목과 일치 한다는 보장은 없습니다 . 다른 스레드는 UI 요소를 업데이트하기 위해이 값을 읽으려고 시도 할 수 있습니다.

Decimal totalAmount;
lock (_locker) totalAmount = _totalAmount;
UpdateUI(_dict.Count, totalAmount);

totalAmount사전에 주문의 수에 일치하지 않을 수 있습니다. 표시된 통계가 잘못되었을 수 있습니다. 이 시점 lock에서 사전 업데이트를 포함하도록 보호를 확장해야합니다 .

// Thread A
lock (_locker)
{
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

// Thread B
int ordersCount;
Decimal totalAmount;
lock (_locker)
{
    ordersCount = _dict.Count;
    totalAmount = _totalAmount;
}
UpdateUI(ordersCount, totalAmount);

이 코드는 완벽하게 안전하지만 a를 사용하면 얻을 수있는 모든 이점 ConcurrentDictionary이 사라졌습니다.

  1. Dictionary내부의 내부 잠금 ConcurrentDictionary이 이제 낭비되고 중복 되기 때문에 성능을 normal보다 사용하는 것보다 나빠졌습니다 .
  2. 공유 변수의 보호되지 않은 사용을 위해 모든 코드를 검토해야합니다.
  3. 어색한 API (TryAdd ?, AddOrUpdate?)를 사용하고 있습니다.

따라서 내 조언은 : Dictionary+로 시작 하고이 옵션이 실제로 실행 가능한 경우 lock나중에 ConcurrentDictionary성능 최적화로 업그레이드하는 옵션을 유지하십시오 . 많은 경우에 그렇지 않습니다.


0

우리는 비슷한 1 시간마다 다시이 채워진 다음 여러 클라이언트 스레드에 의해 읽기 캐시 수집 ConcurrentDictionary, 사용했던 용액에을 (가)에 대한 예 스레드 안전인가?질문.

이를 ReadOnlyDictionary로 변경하면   전반적인 성능이 향상되었습니다.


ReadOnlyDictionary는 다른 IDictionary의 래퍼입니다. 후드 아래에서 무엇을 사용하십니까?
Lu55

@ Lu55에서는 MS .Net 라이브러리를 사용하고 있으며 사용 가능한 / 적용 가능한 사전 중에서 선택했습니다. 나는 ReadOnlyDictionary의 MS 내부 구현에 대한 인식하지 오전
마이클 Freidgeim
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.