스레드로부터 안전한 사전을 구현하는 가장 좋은 방법은 무엇입니까?


109

IDictionary에서 파생하고 개인 SyncRoot 개체를 정의하여 C #에서 스레드로부터 안전한 사전을 구현할 수있었습니다.

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public object SyncRoot
    {
        get { return syncRoot; }
    } 

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
    }

    // more IDictionary members...
}

그런 다음 내 소비자 (다중 스레드) 전체에서이 SyncRoot 개체를 잠급니다.

예:

lock (m_MySharedDictionary.SyncRoot)
{
    m_MySharedDictionary.Add(...);
}

나는 그것을 작동시킬 수 있었지만 이로 인해 추악한 코드가 발생했습니다. 제 질문은 스레드로부터 안전한 사전을 구현하는 더 좋고 우아한 방법이 있습니까?


3
그것에 대해 못생긴 것은 무엇입니까?
Asaf R

1
나는 그가 SharedDictionary 클래스의 소비자 내에서 자신의 코드 전체에 걸쳐 가지고있는 모든 잠금 문을 참조하고 있다고 생각합니다. 그는 SharedDictionary 개체의 메서드에 액세스 할 때마다 호출 코드를 잠그고 있습니다.
Peter Meyer

Add 메서드를 사용하는 대신 ex- m_MySharedDictionary [ "key1"] = "item1"값을 할당하여 시도해보십시오. 이것은 스레드로부터 안전합니다.
testuser

답변:


43

Peter가 말했듯이 클래스 내부의 모든 스레드 안전성을 캡슐화 할 수 있습니다. 노출하거나 추가하는 모든 이벤트에주의하여 잠금 외부에서 호출되도록해야합니다.

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
        OnItemAdded(EventArgs.Empty);
    }

    public event EventHandler ItemAdded;

    protected virtual void OnItemAdded(EventArgs e)
    {
        EventHandler handler = ItemAdded;
        if (handler != null)
            handler(this, e);
    }

    // more IDictionary members...
}

편집 : MSDN 문서는 열거가 본질적으로 스레드로부터 안전하지 않다고 지적합니다. 이것이 클래스 외부에 동기화 개체를 노출하는 한 가지 이유가 될 수 있습니다. 접근하는 또 다른 방법은 모든 구성원에 대해 작업을 수행하고 구성원 열거를 잠그는 몇 가지 방법을 제공하는 것입니다. 이 문제는 해당 함수에 전달 된 작업이 사전의 일부 구성원을 호출하는지 여부를 알 수 없다는 것입니다 (교착 상태가 발생할 수 있음). 동기화 개체를 노출하면 소비자가 이러한 결정을 내릴 수 있으며 클래스 내부의 교착 상태를 숨기지 않습니다.


@fryguybob : 실제로 동기화 개체를 노출하는 유일한 이유는 열거 형이었습니다. 관례 적으로 컬렉션을 열거 할 때만 해당 개체에 대한 잠금을 수행합니다.
GP.

1
사전이 너무 크지 않은 경우 복사본을 열거하고 클래스에 내장 할 수 있습니다.
fryguybob

2
내 사전이 너무 크지 않아서 그게 트릭이라고 생각합니다. 내가 한 것은 개인 사전의 사본과 함께 Dictionary의 새 인스턴스를 반환하는 CopyForEnum ()이라는 새 공용 메서드를 만드는 것입니다. 이 메서드는 enumarations를 위해 호출되었고 SyncRoot가 제거되었습니다. 감사!
GP.

12
또한 사전 작업이 세분화되는 경향이 있기 때문에 본질적으로 스레드 안전 클래스가 아닙니다. if (dict.Contains (whatever)) {dict.Remove (whatever); dict.Add (whatever, newval); }은 확실히 발생하기를 기다리는 경쟁 조건입니다.
plinth

207

동시성을 지원하는 .NET 4.0 클래스의 이름은 ConcurrentDictionary.


4
자신의 닷넷 솔루션이있는 경우 응답으로이 표시를하십시오, 당신은 사용자 정의 dictoniary이 필요하지 않습니다
알베르토 레온

27
(다른 답변은 .NET 4.0 (2010 년 출시) 이전에 작성되었습니다.)
Jeppe Stig Nielsen

1
안타깝게도 잠금이없는 솔루션이 아니므로 SQL Server CLR 안전 어셈블리에서는 쓸모가 없습니다. 여기에 설명 된 것과 같은 것이 필요합니다 : cse.chalmers.se/~tsigas/papers/Lock-Free_Dictionary.pdf 또는 아마도이 구현 : github.com/hackcraft/Ariadne
Triynko

2
정말 오래되었지만 ConcurrentDictionary와 Dictionary를 사용하면 성능이 크게 저하 될 수 있다는 점에 유의하는 것이 중요합니다. 이는 비용이 많이 드는 컨텍스트 전환의 결과 일 가능성이 높으므로 사용하기 전에 스레드로부터 안전한 사전이 필요한지 확인하십시오.
17:37에

60

내부적으로 동기화하려는 시도는 추상화 수준이 너무 낮기 때문에 거의 확실하지 않습니다. 다음과 같이 AddContainsKey작업을 개별적으로 스레드로부터 안전하게 만든다고 가정합니다 .

public void Add(TKey key, TValue value)
{
    lock (this.syncRoot)
    {
        this.innerDictionary.Add(key, value);
    }
}

public bool ContainsKey(TKey key)
{
    lock (this.syncRoot)
    {
        return this.innerDictionary.ContainsKey(key);
    }
}

그러면 여러 스레드에서 스레드로부터 안전한 것으로 추정되는 코드 비트를 호출하면 어떻게됩니까? 항상 정상적으로 작동합니까?

if (!mySafeDictionary.ContainsKey(someKey))
{
    mySafeDictionary.Add(someKey, someValue);
}

간단한 대답은 아니오입니다. 어느 시점에서 Add메서드는 키가 사전에 이미 있음을 나타내는 예외를 throw합니다. 스레드로부터 안전한 사전을 사용하는 방법은 무엇입니까? 각 작업이 스레드로부터 안전하기 때문에 두 작업의 조합은 그렇지 않습니다. 다른 스레드가 호출 ContainsKeyAdd 입니다.

즉, 이러한 유형의 시나리오를 올바르게 작성 하려면 사전 외부 에 잠금이 필요합니다.

lock (mySafeDictionary)
{
    if (!mySafeDictionary.ContainsKey(someKey))
    {
        mySafeDictionary.Add(someKey, someValue);
    }
}

하지만 이제는 외부 잠금 코드를 작성해야하므로 내부 동기화와 외부 동기화가 혼합되어 항상 불명확 한 코드 및 교착 상태와 같은 문제가 발생합니다. 따라서 궁극적으로 다음 중 하나를 수행하는 것이 좋습니다.

  1. 일반을 사용하고 Dictionary<TKey, TValue>외부에서 동기화하여 복합 작업을 포함하거나

  2. 메서드 IDictionary<T>와 같은 작업을 결합 하는 다른 인터페이스 (예 : 아님 )를 사용 하여 새 스레드로부터 안전한 래퍼 AddIfNotContained를 작성하여 작업을 결합 할 필요가 없습니다.

(나는 나 자신 # 1과 함께가는 경향이있다)


10
.NET 4.0에는 표준 컬렉션과 다른 인터페이스를 가진 컬렉션 및 사전과 같은 스레드로부터 안전한 컨테이너 전체가 포함된다는 점을 지적 할 가치가 있습니다 (즉, 위의 옵션 2를 수행합니다).
Greg Beech

1
또한 기본 클래스가 폐기 된시기를 알 수있는 적절한 열거자를 설계하는 경우 조잡한 잠금이 제공하는 세분성만으로도 단일 작성자 다중 판독기 접근 방식에 충분할 수 있습니다 (사전을 작성하고 싶은 방법). 처리되지 않은 열거자가 존재하면 사전을 복사본으로 대체해야합니다).
supercat 2012-08-03

6

속성을 통해 개인 잠금 개체를 게시해서는 안됩니다. 잠금 개체는 랑데부 지점 역할을 할 목적으로 만 비공개로 존재해야합니다.

표준 잠금을 사용하여 성능이 좋지 않은 것으로 판명되면 Wintellect의 Power Threading 잠금 모음이 매우 유용 할 수 있습니다.


5

설명하고있는 구현 방법에는 몇 가지 문제가 있습니다.

  1. 동기화 개체를 노출해서는 안됩니다. 그렇게하면 소비자가 물건을 잡고 잠그고 건배하게됩니다.
  2. 스레드 안전 클래스를 사용하여 스레드 안전이 아닌 인터페이스를 구현하고 있습니다. IMHO 이것은 길을 잃을 것입니다.

개인적으로 스레드 안전 클래스를 구현하는 가장 좋은 방법은 불변성을 이용하는 것입니다. 스레드 안전성과 관련하여 발생할 수있는 문제의 수를 실제로 줄여줍니다. 확인 에릭 Lippert의의 블로그를 자세한 내용은.


3

소비자 개체에서 SyncRoot 속성을 잠글 필요가 없습니다. 사전의 메소드 내에있는 잠금으로 충분합니다.

자세히 설명하려면 : 결국에는 사전이 필요한 것보다 더 오랜 시간 동안 잠겨 있다는 것입니다.

귀하의 경우에는 다음과 같은 일이 발생합니다.

스레드 A가 이전 에 SyncRoot에 대한 잠금을 획득했다고 가정합니다. m_mySharedDictionary.Add를 호출 . 그런 다음 스레드 B가 잠금 획득을 시도하지만 차단됩니다. 실제로 다른 모든 스레드는 차단됩니다. 스레드 A는 Add 메서드를 호출 할 수 있습니다. Add 메서드 내의 잠금 문에서 스레드 A는 이미 잠금을 소유하고 있기 때문에 다시 잠금을 얻을 수 있습니다. 메소드 내에서 잠금 컨텍스트를 종료 한 다음 메소드 외부에서 스레드 A는 다른 스레드가 계속할 수 있도록 모든 잠금을 해제했습니다.

SharedDictionary 클래스 Add 메서드 내의 잠금 문이 동일한 효과를 가지므로 모든 소비자가 Add 메서드를 호출하도록 허용 할 수 있습니다. 이 시점에서 중복 잠금이 있습니다. 연속적으로 발생하도록 보장해야하는 사전 개체에 대해 두 가지 작업을 수행해야하는 경우 사전 메서드 중 하나 외부에서 SyncRoot 만 잠급니다.


1
사실이 아님 ... 내부적으로 스레드로부터 안전한 두 작업을 차례로 수행한다고해서 전체 코드 블록이 스레드로부터 안전하다는 의미는 아닙니다. 예를 들어 : if (! myDict.ContainsKey (someKey)) {myDict.Add (someKey, someValue); } 포함 키 및 추가도 스레드 세이프되지 않습니다
Tim

귀하의 요점은 정확하지만 내 대답 및 질문과 관련이 없습니다. 질문을 보면 ContainsKey 호출에 대해 이야기하지 않으며 내 대답도 없습니다. 내 대답은 원래 질문의 예에 표시된 SyncRoot에 대한 잠금을 획득하는 것입니다. 잠금 문의 컨텍스트 내에서 하나 이상의 스레드 안전 작업이 실제로 안전하게 실행됩니다.
Peter Meyer

그가하는 일은 사전에 추가하는 것이지만 "// 더 많은 IDictionary 멤버 ..."가 있기 때문에 어떤 시점에서 그는 사전에서 데이터를 다시 읽고 싶어 할 것이라고 가정합니다. 이 경우 외부에서 액세스 할 수있는 잠금 장치가 있어야합니다. 사전 자체의 SyncRoot인지 또는 잠금에만 사용되는 다른 객체인지는 중요하지 않지만 이러한 체계가 없으면 전체 코드가 스레드로부터 안전하지 않습니다.
Tim

외부 잠금 메커니즘은 질문에서 그의 예에서 보여준 것과 같습니다. lock (m_MySharedDictionary.SyncRoot) {m_MySharedDictionary.Add (...); }-다음을 수행하는 것이 완벽하게 안전합니다. lock (m_MySharedDictionary.SyncRoot) {if (! m_MySharedDictionary.Contains (...)) {m_MySharedDictionary.Add (...); }} 즉, 외부 잠금 메커니즘은 공용 속성 SyncRoot에서 작동하는 잠금 문입니다.
피터 마이어

0

왜 사전을 다시 만들어 보지 않겠습니까? 읽기가 다중 쓰기 인 경우 잠금은 모든 요청을 동기화합니다.

    private static readonly object Lock = new object();
    private static Dictionary<string, string> _dict = new Dictionary<string, string>();

    private string Fetch(string key)
    {
        lock (Lock)
        {
            string returnValue;
            if (_dict.TryGetValue(key, out returnValue))
                return returnValue;

            returnValue = "find the new value";
            _dict = new Dictionary<string, string>(_dict) { { key, returnValue } };

            return returnValue;
        }
    }

    public string GetValue(key)
    {
        string returnValue;

        return _dict.TryGetValue(key, out returnValue)? returnValue : Fetch(key);
    }

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.