2012-03-28 8 views
9

値のキャッシングにConcurrentDictionary<TKey, TValue>を使用するいくつかのケースがありますが、多くの場合、ConcurrentDictionary<TKey, TValue>.GetOrAdd(TKey, Func<TKey, TValue>)を使用してキャッシュに追加するかどうかを決定するために値の検証を実行する必要があります。`ConcurrentDictionary.GetOrAdd`に値を追加しないように指示する方法はありますか?

通常の線に沿って:

private readonly ConcurrentDictionary<Type, ISomeObject> someObjectCache = 
    new ConcurrentDictionary<Type, ISomeObject>(); 
public ISomeObject CreateSomeObject(Type someType) 
{ 
    return someObjectCache.GetOrAdd(someType, type => 
    { 
     if(!Attribute.IsDefined(someType, typeof(SomeAttribute)) 
      // Do something here to avoid the instance from being added to 
      // `someObjectCache` 

     ISomeObject someObject; 
     // Typical factory functionality goes here 
     return someObject; 
    }); 
} 

今日はこれを処理する方法は、正常に動作するように見える例外をスローすることですが、私はクリーンなアプローチが欲しい(私は設定することができます多分フラグまたは特定の値を返すように設定してラムダ内からGetOrAddを取り消すことができます(ただし、現実的には完全な方法で置き換えることができます)。

nullを返す他のLINQメソッドの経験に基づいて、チェックされずに値が追加されます(GetOrAddのILを読み取ると、同じ問題が発生するようです)。それがうまくいくとは思わない。

GetOrAddを使用して例外を使用して追加をキャンセルしないようにする方法はありますか?

答えて

10

私が読んだところでは、no guarantee that the Add factory method will only be called a single time amongst all callers to Get for the same keyがあります。

そのページから関連する部分が下にあり、ここで引用:

また、(処理鍵のうち、TValue)ConcurrentDictionary のすべてのメソッドは、スレッドセーフであるが、全ての方法は、具体的には、アトミックされていませんGetOrAddと AddOrUpdate。これらのメソッドに渡されるユーザーデリゲートは、辞書の内部ロックの外側で呼び出される です。 、

1)スレッドAがGetOrAddを呼び出して何のアイテムを見つけていないとで を追加するための新しいアイテムを作成します。この一連のイベントが発生するので、それは 可能である(これは、すべてのスレッドをブロックから、未知のコードを防ぐ に行われます) valueFactoryデリゲートを呼び出します。同時にGetOrAddを呼び出すthreadB

2)は、そのValueFactoryのデリゲートが呼び出さ であり、それはスレッドAの前に内部ロックに到着し、そのためその 新しいキーと値のペアは、辞書に追加されます。

3)スレッドAのユーザーデリゲートが完了すると、スレッドが ロックに到着したが、今の項目がすでに存在していることを見て

4)スレッドAは、「取得」を実行し、以前に を加えたデータを返します。 threadBによって。

したがって、 GetOrAddによって返されるデータは、スレッドの valueFactoryによって作成されたものと同じデータであるとは限りません。 AddOrUpdate が呼び出されると、同様の一連のイベントが発生する可能性があります。

私がこれを読んでいるところは、あなたのAdd Delegateでいくつかのロックを呼び出しても、あなたのaddから返された値が実際に使用される値であるという保証はありません。

だから、あなたはそれ以上のロックを追加する必要はありません、代わりに、おそらく次のパターンを使用することができます。

private ConcurrentDictionary<Type, ISomeObject> someObjectCache = 
    new ConcurrentDictionary<Type, ISomeObject>(); 
public ISomeObject CreateSomeObject(Type someType) 
{ 

    ISomeObject someObject; 
    if (someObjectCache.TryGet(someType, out someObject)) 
    { 
     return someObject; 
    } 

    if (Attribute.IsDefined(someType, typeof(SomeAttribute)) 
    { 
     // init someObject here 
     someObject = new SomeObject(); 

     return someObjectCache.GetOrAdd(someType, someObject); // If another thread got through here first, we'll return their object here. 
    } 

    // fallback functionality goes here if it doesn't have your attribute. 
} 

はい、これは潜在的に複数回作成する新しいオブジェクトのためのいくつかの潜在的になります呼び出し元は複数の呼び出しがあってもすべて同じ結果を受け取ることになります。 GetOrAddと同じです。コンピュータサイエンスの

+0

しかし、いったん必要なすべてのスレッドロック機構でそれを乱雑にすると、むしろ乱雑になります(私のものはロックを必要としません...同じ程度ではありません)。私は、より洗練されたソリューションを探しています。自分のバージョンのConcurrentDictionaryを効果的にロールバックする必要はありません。 –

+0

私の答えを更新 - GetOrAddは、内部配列ビットを扱う場合にのみ、Add部分をロックしません。だから、あなたが意図しているのは、検証だけを追加することだとすれば、ここにはロックが必要ないはずです。 –

+0

はい、私はその部分を読んだことがありますが、これはまだ私が探しているものではありません。あまりにも多くのコードを書いたければ、私は自分でロックを処理するでしょう。 –

3

すべての問題は、今辞書に格納された値は、関数ある間接

// the dictionary now stores functions 
private readonly ConcurrentDictionary<Type, Func<ISomeObject>> someObjectCache = 
    new ConcurrentDictionary<Type, Func<ISomeObject>>(); 

public ISomeObject CreateSomeObject(Type someType) { 
    return someObjectCache.GetOrAdd(someType, _ => { 
    if(ShouldCache(someType)) { 
     // caching should be used 
     // return a function that returns a cached instance 
     var someObject = Create(someType); 
     return() => someObject; 
    } 
    else { 
     // no caching should be used 
     // return a function that always creates a new instance 
     return() => Create(someType); 
    } 
    })(); // call the returned function 
} 

private bool ShouldCache(Type someType) { 
    return Attribute.IsDefined(someType, typeof(SomeAttribute)); 
} 

private ISomeObject Create(Type someType) { 
    // typical factory functionality ... 
} 

の別のレベルによって解決することができます。キャッシングが発生したくない場合、関数は常に新しいインスタンスを作成します。キャッシングが発生するようにするには、この関数はキャッシュされたインスタンスを返します。

+0

これはうまくいくのですか?ここで行ったことは、チェックを関数に移して、とにかくその型のインスタンスを返すことだけです。おそらくもっと説明が必要です。 –

+0

確かに、もう少し追加します.... –

+0

別の機能にチェックを移動することは、質問の中心とは関係ありません。それはちょっとしたリファクタリングです。 –

関連する問題