31

スレッドセーフな静的キャッシュとして並行ディクショナリを使用していますが、次の動作に気づきました。

GetOrAddのMSDNドキュメントから:

異なるスレッドでGetOrAddを同時に呼び出す場合、addValueFactoryは複数回呼び出される可能性がありますが、そのキーと値のペアがすべての呼び出しでディクショナリに追加されるとは限りません。

工場が一度だけ呼び出されることを保証できるようにしたいと思います。独自の個別の同期に頼らずに(たとえば、valueFactory内でロックする)、ConcurrentDictionary APIを使用してこれを行う方法はありますか?

私のユースケースは、valueFactoryが動的モジュール内で型を生成しているため、同じキーの2つのvalueFactoriesが同時に実行される場合、次のようにヒットします。

System.ArgumentException: Duplicate type name within an assembly.

4

2 に答える 2

44

次のように入力されたディクショナリを使用できます。ConcurrentDictionary<TKey, Lazy<TValue>>すると、値ファクトリは、でLazy<TValue>初期化されたオブジェクトを返します。これは、指定しない場合にLazyThreadSafetyMode.ExecutionAndPublication使用されるデフォルトのオプションです。Lazy<TValue>あなたがLazyに伝えていることを指定することにより、LazyThreadSafetyMode.ExecutionAndPublication1つのスレッドだけがオブジェクトの値を初期化して設定することができます。

これにより、オブジェクトConcurrentDictionaryのインスタンスが1つだけ使用され、オブジェクトは複数のスレッドをその値の初期化から保護します。Lazy<TValue>Lazy<TValue>

すなわち

var dict = new ConcurrentDictionary<int, Lazy<Foo>>();
dict.GetOrAdd(key,  
    (k) => new Lazy<Foo>(valueFactory)
);

欠点は、ディクショナリ内のオブジェクトにアクセスするたびに*.Valueを呼び出す必要があることです。これを支援するいくつかの拡張機能があります。

public static class ConcurrentDictionaryExtensions
{
    public static TValue GetOrAdd<TKey, TValue>(
        this ConcurrentDictionary<TKey, Lazy<TValue>> @this,
        TKey key, Func<TKey, TValue> valueFactory
    )
    {
        return @this.GetOrAdd(key,
            (k) => new Lazy<TValue>(() => valueFactory(k))
        ).Value;
    }

    public static TValue AddOrUpdate<TKey, TValue>(
        this ConcurrentDictionary<TKey, Lazy<TValue>> @this,
        TKey key, Func<TKey, TValue> addValueFactory,
        Func<TKey, TValue, TValue> updateValueFactory
    )
    {
        return @this.AddOrUpdate(key,
            (k) => new Lazy<TValue>(() => addValueFactory(k)),
            (k, currentValue) => new Lazy<TValue>(
                () => updateValueFactory(k, currentValue.Value)
            )
        ).Value;
    }

    public static bool TryGetValue<TKey, TValue>(
        this ConcurrentDictionary<TKey, Lazy<TValue>> @this,
        TKey key, out TValue value
    )
    {
        value = default(TValue);

        var result = @this.TryGetValue(key, out Lazy<TValue> v);

        if (result) value = v.Value;

        return result;
   }

   // this overload may not make sense to use when you want to avoid
   //  the construction of the value when it isn't needed
   public static bool TryAdd<TKey, TValue>(
       this ConcurrentDictionary<TKey, Lazy<TValue>> @this,
       TKey key, TValue value
   )
   {
       return @this.TryAdd(key, new Lazy<TValue>(() => value));
   }

   public static bool TryAdd<TKey, TValue>(
       this ConcurrentDictionary<TKey, Lazy<TValue>> @this,
       TKey key, Func<TKey, TValue> valueFactory
   )
   {
       return @this.TryAdd(key,
           new Lazy<TValue>(() => valueFactory(key))
       );
   }

   public static bool TryRemove<TKey, TValue>(
       this ConcurrentDictionary<TKey, Lazy<TValue>> @this,
       TKey key, out TValue value
   )
   {
       value = default(TValue);

       if (@this.TryRemove(key, out Lazy<TValue> v))
       {
           value = v.Value;
           return true;
       }
       return false;
   }

   public static bool TryUpdate<TKey, TValue>(
       this ConcurrentDictionary<TKey, Lazy<TValue>> @this,
       TKey key, Func<TKey, TValue, TValue> updateValueFactory
   )
   {
       if (!@this.TryGetValue(key, out Lazy<TValue> existingValue))
           return false;

       return @this.TryUpdate(key,
           new Lazy<TValue>(
               () => updateValueFactory(key, existingValue.Value)
           ),
           existingValue
       );
   }
}
于 2012-09-26T22:21:06.453 に答える
6

これは、ノンブロッキングアルゴリズムでは珍しいことではありません。それらは基本的に、を使用して競合がないことを確認する条件をテストしInterlock.CompareExchangeます。CASが成功するまでループします。ノンブロッキングアルゴリズムConcurrentQueueの良い入門書として(4)ページをご覧ください

簡単な答えはノーです。競合しているコレクションに追加するために複数回試行する必要があるのは、獣の性質です。値を渡すという他の過負荷を使用する以外に、おそらくダブルロック/メモリバリアを使用して、値ファクトリ内の複数の呼び出しから保護する必要があります。

于 2012-09-26T22:16:43.643 に答える