質問

IDictionary から派生し、プライベート SyncRoot オブジェクトを定義することで、C# でスレッドセーフな Dictionary を実装することができました。

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

動作させることはできましたが、醜いコードになってしまいました。私の質問は、スレッドセーフな辞書を実装する、より優れた、より洗練された方法はあるのかということです。

役に立ちましたか?

解決

ピーターが言ったように、すべてのスレッドセーフをクラス内にカプセル化できます。公開または追加するイベントに注意し、ロックの外側で呼び出されるようにする必要があります。

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のドキュメントでは、列挙は本質的にスレッドセーフではないことが指摘されています。これが、同期オブジェクトをクラスの外部に公開する理由の1つです。アプローチする別の方法は、すべてのメンバーに対してアクションを実行し、メンバーの列挙をロックする方法を提供することです。これの問題は、その関数に渡されたアクションが辞書のメンバーを呼び出すかどうかがわからないことです(デッドロックが発生します)。同期オブジェクトを公開することで、コンシューマーはこれらの決定を下すことができ、クラス内のデッドロックを隠しません。

他のヒント

同時実行性をサポートする.NET 4.0クラスの名前は ConcurrentDictionary

内部的に同期しようとすると、抽象化のレベルが低すぎるため、ほぼ間違いなく不十分です。次のようにAddおよびContainsKey操作を個別にスレッドセーフにするとします。

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

単純な答えはノーです。ある時点で、Dictionary<TKey, TValue>メソッドは、キーが辞書に既に存在することを示す例外をスローします。スレッドセーフな辞書を使用すると、どのようになりますか?各操作がスレッドセーフであるという理由だけで、2つの操作の組み合わせはそうではありません。別のスレッドがIDictionary<T>AddIfNotContainedの呼び出しの間に操作を変更する可能性があるためです。

このタイプのシナリオを正しく記述するには、辞書のロックが必要です 、たとえば

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

しかし、今では、外部的にロックするコードを書かなければならないので、内部と外部の同期を混同しているので、常に不明瞭なコードやデッドロックなどの問題につながります。したがって、最終的には次のいずれかの方がおそらく優れています。

  1. 通常の<=>を使用して外部で同期し、その上で複合操作を囲むか、

  2. <=>メソッドなどの操作を結合する異なるインターフェイス(つまり、<=>ではない)で新しいスレッドセーフラッパーを作成し、操作を結合する必要がないようにします。

(私は自分で#1に行く傾向があります)

プライベートロックオブジェクトをプロパティを介して公開しないでください。ロックオブジェクトは、ランデブーポイントとして機能することのみを目的として、プライベートに存在する必要があります。

標準ロックを使用してパフォーマンスが低下することが判明した場合、Wintellectの Power Threading ロックのコレクションは非常に便利です。

あなたが説明している実装方法にはいくつかの問題があります。

  1. 同期オブジェクトを公開しないでください。そうすることで、消費者がオブジェクトを掴んでロックを取得できるようになります。そうすれば、あなたは満足です。
  2. スレッドセーフなクラスを使用して非スレッドセーフなインターフェイスを実装しています。私の意見では、これは将来的に費用がかかります

個人的には、スレッド セーフなクラスを実装する最良の方法は不変性を使用することだと考えています。これにより、スレッド セーフに関して発生する可能性のある問題の数が大幅に減少します。チェックアウト エリック・リッパートのブログ 詳細については。

コンシューマオブジェクトのSyncRootプロパティをロックする必要はありません。辞書のメソッド内にあるロックで十分です。

詳しく説明する: 最終的に起こるのは、辞書が必要以上に長い時間ロックされることです。

あなたのケースで起こることは次のとおりです:

スレッドAが、m_mySharedDictionary.Addの呼び出しの 前にSyncRootのロックを取得します。次に、スレッドBはロックの取得を試みますが、ブロックされます。実際、他のすべてのスレッドはブロックされます。スレッドAは、Addメソッドを呼び出すことができます。 Addメソッド内のロックステートメントで、スレッドAは既にロックを所有しているため、ロックを再度取得できます。メソッド内でメソッドの外部でロックコンテキストを終了すると、スレッドAはすべてのロックを解放し、他のスレッドが続行できるようにします。

SharedDictionaryクラスのAddメソッド内のロックステートメントは同じ効果があるため、すべてのコンシューマーがAddメソッドを呼び出すことを許可できます。この時点で、冗長ロックがあります。連続して発生することが保証される必要があるディクショナリオブジェクトに対して2つの操作を実行する必要がある場合にのみ、ディクショナリメソッドの1つの外部で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);
    }
ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top