2009-08-10 11 views
21

のが出発点として、メモ化を機能させるWes Dyer'sアプローチを見てみましょう:スレッドセーフなメモ化

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var map = new Dictionary<A, R>(); 
    return a => 
    { 
     R value; 
     if (map.TryGetValue(a, out value)) 
     return value; 
     value = f(a); 
     map.Add(a, value); 
     return value; 
    }; 
} 

問題であり、複数のスレッドからそれを使用した場合、我々はトラブルに取得することができます。

Func<int, int> f = ... 
var f1 = f.Memoize(); 
... 
in thread 1: 
var y1 = f1(1); 
in thread 2: 
var y2 = f1(1); 
// We may be recalculating f(1) here! 

これを回避しようとしましょう。 mapにロック:それは一度に多くの異なる引数にf1を計算するから私たちを防ぐため

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var map = new Dictionary<A, R>(); 
    return a => 
    { 
     R value; 
     lock(map) 
     { 
     if (map.TryGetValue(a, out value)) 
      return value; 
     value = f(a); 
     map.Add(a, value); 
     } 
     return value; 
    }; 
} 

することは、明らかに恐ろしい考えです。 aのロックは、aに値の型がある場合には機能しません(そして、どんな場合でも、私たちはaを制御せず、外部コードもロックされる可能性があるため)。

hereを参照)遅延評価のためのLazy<T>クラスを仮定:

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var map = new Dictionary<A, Lazy<R>>(); 
    return a => 
    { 
     Lazy<R> result; 
     lock(map) 
     { 
     if (!map.TryGetValue(a, out result)) 
     { 
      result =() => f(a); 
      map.Add(a, result); 
     } 
     } 
     return result.Value; 
    }; 
} 

や同期のためのオブジェクトの追加辞書を保つ:

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var map = new Dictionary<A, R>(); 
    var mapSync = new Dictionary<A, object>(); 
    return a => 
    { 
     R value; 
     object sync; 
     lock(mapSync) 
     { 
     if (!mapSync.TryGetValue(a, out sync)) 
     { 
      sync = new object(); 
      mapSync[a] = sync; 
     } 
     } 
     lock(map) 
     { 
     if (map.TryGetValue(a, out value)) 
      return value; 
     } 
     lock(sync) 
     { 
     value = f(a); 
     lock(map) 
     { 
      map[a] = value; 
     } 
     return value; 
     } 
    }; 
} 
ここ

は、私は考えることができる2つのオプションがあります

もっと良いオプションはありますか?

答えて

33

.net 4.0のConcurrentDictionary<A, R>を使用すると、不要なLazy<R>がなくなります。
鍵はGetOrAdd(A, Func<A, R>)で、美しく些細なラムダになります。

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var cache = new ConcurrentDictionary<A, R>(); 
    return a => cache.GetOrAdd(a, f); 
}; 

更新上記溶液は、同時に複数の読者にオーバーヘッドの最小値と&ライターを可能にし。しかし、f(a)が(計算中の期間中)同じ値に対して複数回実行されるのを防ぐことはできません。

これが重要な場合は、値をLazy<R>にラップすることができますが、すべての読み取りに費用が発生します。 Lazyバージョンが、720ms - 正規Dictionaryと同じ - 万人のための

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var cache = new ConcurrentDictionary<A, Lazy<R>>(); 
    return a => cache.GetOrAdd(a, new Lazy<R>(() => f(a))).Value; 
} 

更新タイミングテストは、事前に人口1000アイテムキャッシュショーConcurrentDictionaryため19msの読み取り。

これがあまりにも急に聞こえる場合は、より複雑なソリューションで両方の世界の利点を得ることができます。

私はFの呼び出し回数を制限する彼の溶液(A)から抽出された再利用可能なコンポーネントを提供したかったナイジェル・タッチの 優れ答え、上拡大
public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var cache = new ConcurrentDictionary<A, R>(); 
    var syncMap = new ConcurrentDictionary<A, object>(); 
    return a => 
    { 
     R r; 
     if (!cache.TryGetValue(a, out r)) 
     { 
      var sync = syncMap.GetOrAdd(a, new object()); 
      lock (sync) 
      { 
       r = cache.GetOrAdd(a, f); 
      } 
      syncMap.TryRemove(a, out sync); 
     } 
     return r; 
    }; 
} 
+2

私はこれが優れた答えだと言いたいと思います。ありがとうございました! –

1

いいえ、それは良い選択肢ではありません。

怠惰な評価のバージョンは、とにかくすぐに評価するので無意味です。同期ディクショナリのバージョンは、使用する前にロック内のマップディクショナリを保護していないため、正しく動作しません。

恐ろしいと呼ばれるバージョンが実際には最適なオプションです。一度に1つのスレッドだけがアクセスできるように、ロック内のマップディクショナリを保護する必要があります。辞書はスレッドセーフではありません。したがって、別のスレッドがスレッドを変更している間に、あるスレッドにスレッドを読み込ませる場合、問題が発生します。

マップオブジェクトでlockを使用しても、マップオブジェクト自体は保護されません。ロック内でコードを実行するには、一度に複数のスレッドを保持する識別子としてマップ参照を使用するだけです。オブジェクトを変更しているコードだけでなく、オブジェクトにアクセスするすべてのコードをロック内に配置する必要があります。

+0

私はレイジー評価版を修正しました。 –

+0

同期辞書のバージョン。 –

+0

値が常にすぐに評価されるため、遅延評価版はまだ価値がありません。同期辞書のバージョンはまだ安全ではありません。異なるスレッドが同じキーのオブジェクトを作成し、一方が他方を上書きするためです。 – Guffa

10

あなたはすでにLazy<T>タイプは、私はあなたにも使うことができるようにあなたは、.NET 4.0を使用していると仮定していることがある場合はConcurrentDictionary<A,R>

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var map = new ConcurrentDictionary<A, Lazy<R>>(); 
    return a => 
    { 
     Lazy<R> lazy = new Lazy<R>(() => f(a), LazyExecutionMode.EnsureSingleThreadSafeExecution); 
     if(!map.TryAdd(a, lazy)) 
     { 
     return map[a].Value; 
     } 
     return lazy.Value; 
    }; 
} 
0

あなたは読みました、スレッドセーフにするために記事に関連するcomment from Dyer

おそらくMemoizeをスレッドセーフにする最も簡単な方法は、マップにロックを設定することです。

これにより、memoizedされている関数は、個別の引数のセットごとに1回だけ実行されます。

私のRoboRallyゲームの例では、実際には関数のメモを「サロゲートシングルトン」として使用していました。ファクトリが静的でない限り、ファクトリインスタンスごとに1つのインスタンスが存在する可能性があるので、実際にはシングルトンではありません。しかし、それはまさに私が望んでいたものです。

+0

はい、それは一番簡単な方法です。私は特にそれについて悪いことを言った:それは同時に異なる引数で関数を評価することから私たちを停止します。 –

1

同じ値を2回計算する必要はなく、多くのスレッドが値を計算したり、値を同時に取得したりすることができます。これを行うには、何らかの種類の条件変数とファイングレインロックシステムを使用する必要があります。

Heres the idea。値が存在しない場合は、値を同期マップに入れ、その値を必要とするスレッドはそれを待つでしょう。さもなければ、現在の値を取得するだけです。この方法でマップのロックを最小限に抑えて、値を照会し値を返すことができます。

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
    { 
     var map = new Dictionary<A, R>(); 
     var mapSync = new Dictionary<A, object>(); 
     return a => 
     { 
      R value; 
      object sync = null; 
      bool calc = false; 
      bool wait = false; 
      lock (map) 
      { 
       if (!map.TryGetValue(a, out value)) 
       { 
        //its not in the map 
        if (!mapSync.TryGetValue(a, out sync)) 
        { 
         //not currently being created 
         sync = new object(); 
         mapSync[a] = sync; 
         calc = true; 

        } 
        else 
        { 
         calc = false; 
         wait = true; 
        } 
       } 
      } 
      if(calc) 
      { 
       lock (sync) 
       { 
        value = f(a); 
        lock (map) 
        { 
         map.Add(a, value); 
         mapSync.Remove(a); 
        } 
        Monitor.PulseAll(sync); 
        return value; 
       } 
      } 
      else if (wait) 
      { 
       lock (sync) 
       { 
        while (!map.TryGetValue(a, out value)) 
        { 
         Monitor.Wait(sync); 
        } 
        return value; 
       } 
      } 

      lock (map) 
      { 
       return map[a]; 
      } 

     }; 
    } 

これは簡単な最初の試みですが、私はそれが技術を実証していると思います。ここでは、速度のために追加のメモリを交換しています。

2

レイジーコンストラクタのenumパラメータのためにThomasの答えが.NET 4.0でコンパイルされていないようです。私はそれを以下に改訂しました。私はまた、自分自身の等価比較子を提供するためのオプションのパラメータを追加しました。これは、TInputが独自のEqualsを実装していない場合や、TInputが文字列で、大文字小文字を区別しないようにする場合に便利です。

public static Func<TInput, TResult> Memoize<TInput, TResult>(
     this Func<TInput, TResult> func, IEqualityComparer<TInput> comparer = null) 
    { 
     var map = comparer == null 
         ? new ConcurrentDictionary<TInput, Lazy<TResult>>() 
         : new ConcurrentDictionary<TInput, Lazy<TResult>>(comparer); 

     return input => 
       { 
        var lazy = new Lazy<TResult>(() => func(input), LazyThreadSafetyMode.ExecutionAndPublication); 

        return map.TryAdd(input, lazy) 
           ? lazy.Value 
           : map[input].Value; 
       }; 
    } 

私は私のテストとしてこれを使用して、この方法のいくつかの基本的なテストをした:

public void TestMemoize() 
    { 
     Func<int, string> mainFunc = i => 
            { 
             Console.WriteLine("Evaluating " + i); 
             Thread.Sleep(1000); 
             return i.ToString(); 
            }; 

     var memoized = mainFunc.Memoize(); 

     Parallel.ForEach(
      Enumerable.Range(0, 10), 
      i => Parallel.ForEach(Enumerable.Range(0, 10), j => Console.WriteLine(memoized(i)))); 
    } 

それが正常に動作しているようです。

0

私はSynchronizedConcurrentDictionaryそれと呼ばれ、それは次のようになります。

public class SynchronizedConcurrentDictionary<TKey, TValue> : ConcurrentDictionary<TKey, TValue> 
{ 
    private readonly ReaderWriterLockSlim _cacheLock = new ReaderWriterLockSlim(); 

    public new TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory) 
    { 
     TValue result; 

     _cacheLock.EnterWriteLock(); 
     try 
     { 
      result = base.GetOrAdd(key, valueFactory); 
     } 
     finally 
     { 
      _cacheLock.ExitWriteLock(); 
     } 

     return result; 
    } 
} 

その後Memoize関数は、2つのライナーのようになる。

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var cache = new SynchronizedConcurrentDictionary<A, R>(); 

    return key => cache.GetOrAdd(key, f); 
} 

乾杯!

+0

なぜコメントなしのdownvote?私は派生したコミュニティに役立つものを提供しようとしていました。どうしたの? –

+0

注: "SynchronizedConcurrentDictionary"という名前はおそらく悪いです! ConcurrentDictionaryは、ICollectionへのアクセスが同期(スレッドセーフ)かどうかを示す値を取得するプロパティ "IsSynchronized"を持つICollectionを実装します。 ConcurrentDictionaryはこのプロパティからfalseを返し、読み取ると、SyncRootプロパティは例外をスローします。 「SynchronizedConcurrentDictionary」という名前は、コレクションがSyncRootを介して同期されていることを意味すると解釈できます。これはfalseです。 –

関連する問題