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

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

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



답변

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


답변

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


답변

내부적으로 동기화하려는 시도는 추상화 수준이 너무 낮기 때문에 거의 확실하지 않습니다. 다음과 같이 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과 함께가는 경향이있다)


답변

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

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


답변

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

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

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


답변

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

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

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

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

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


답변

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

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