記憶化,是一種爲了提升應用程序性能的FP技術。程序加速是經過緩存函數的結果實現的,避免了重複計算帶來的額外開銷。緩存
一、如今咱們使用Dictionary做爲緩存結構安全
1 public static Func<T, R> Memoize<T, R>(Func<T, R> func) 2 where T : IComparable 3 { 4 Dictionary<T, R> cache = new Dictionary<T, R>(); 5 return arg => 6 { 7 if (cache.ContainsKey(arg)) 8 return cache[arg]; 9 return (cache[arg] = func(arg)); 10 }; 11 }
1 public static string GetString(string name) 2 { 3 return $"return date {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} string {name}"; 4 }
1 var getStrMemoize = Memoize<string, string>(GetString); 2 Console.WriteLine(getStrMemoize("A")); 3 Thread.Sleep(3000); 4 Console.WriteLine(getStrMemoize("B")); 5 Thread.Sleep(3000); 6 Console.WriteLine(getStrMemoize("A"));
打印結果:併發
1 return date 2020-12-31 08:37:12 string A 2 return date 2020-12-31 08:37:15 string B 3 return date 2020-12-31 08:37:12 string A
能夠看出第三次打印的結果跟第一次打印的結果相同,也就是被緩存在Dictionary中的值。app
在單線程中咱們這樣寫沒有問題,程序順序被執行,Dictionary不存在併發問題,可是當咱們想在多個線程並行時Dictionary不是線程安全集合,會存在線程安全問題。dom
二、如今咱們使用線程安全集合ConcurrentDictionary進行改進:(方法中註釋已經對方法作了說明,在此不重複)函數
1 /// <summary> 2 /// 使用線程安全集合 3 /// </summary> 4 /// <typeparam name="T"></typeparam> 5 /// <typeparam name="R"></typeparam> 6 /// <param name="func"></param> 7 /// 對於字典的修改和寫入操做, ConcurrentDictionary<TKey,TValue> 使用細粒度鎖定以確保線程安全。 8 /// 對字典進行 (讀取操做時,將以無鎖方式執行。) 不過,在 valueFactory 鎖的外部調用委託,以免在鎖定下執行未知代碼時可能產生的問題。 9 /// 所以,對於 GetOrAdd 類上的全部其餘操做而言,不是原子的 ConcurrentDictionary<TKey,TValue> 。 10 /// 因爲在生成值時,另外一個線程能夠插入鍵/值 valueFactory ,所以您不能信任這一點, 11 /// 由於已 valueFactory 執行,其生成的值將插入到字典中並返回。 12 /// 若是 GetOrAdd 在不一樣的線程上同時調用,則 valueFactory 能夠屢次調用,但只會將一個鍵/值對添加到字典中。 13 /// 返回值取決於字典中的鍵是否存在,以及是否在 GetOrAdd 調用以後但在生成值以前由另外一個線程插入了鍵/值 valueFactory 14 /// (若是當前線程檢查到Key不在字典中,那麼會執行生成鍵值;可是在寫入前若是有線程完成了寫入鍵值,當前線程寫入前檢查到有寫入值,則以已寫入的爲準)。 15 /// <returns></returns> 16 public static Func<T, R> MemoizeThreadSafe<T, R>(Func<T, R> func) where T : IComparable 17 { 18 ConcurrentDictionary<T, R> cache = new ConcurrentDictionary<T, R>(); 19 return arg => 20 { 21 return cache.GetOrAdd(arg, a => func(arg)); 22 }; 23 }
1 var getStrMemoize = MemoizeThreadSafe<string, string>(GetString); 2 Console.WriteLine(getStrMemoize("A")); 3 Thread.Sleep(3000); 4 Console.WriteLine(getStrMemoize("B")); 5 Thread.Sleep(3000); 6 Console.WriteLine(getStrMemoize("A"));
打印結果:性能
1 return date 2020-12-31 08:42:46 string A 2 return date 2020-12-31 08:42:49 string B 3 return date 2020-12-31 08:42:46 string A
註解中咱們說明了ConcurrentDictionary是線程安全集合,可是當咱們使用GetOrAdd時,因爲該方法不是原子性的操做,當進行初始化時,可能多個線程同時進行初始化操做,帶來了額外的開銷。this
三、爲解決GetOrAdd非原子性操做重複初始化操做,引入延遲初始化(註解已詳細說明):spa
在看改進方法前咱們先看下Lazy類的用法:線程
1 public class user 2 { 3 public string name { get; set; } 4 }
1 Lazy<user> user = new Lazy<user>(); 2 if (!user.IsValueCreated) 3 Console.WriteLine("user 未建立."); 4 user.Value.name = "test"; 5 if (user.IsValueCreated) 6 Console.WriteLine("user 已建立.");
輸出:
1 user 未建立. 2 user 已建立.
如下爲Lazy類代碼片斷,從代碼咱們看出在對象未使用(value)前,實例並未真正建立:
1 [NonSerialized] 2 private Func<T> m_valueFactory; 3 4 private object m_boxed; 5 6 public T Value 7 { 8 get 9 { 10 return LazyInitValue(); 11 } 12 } 13 private T LazyInitValue() 14 { 15 Boxed boxed = null; 16 try 17 { 18 boxed = CreateValue(); 19 m_boxed = boxed; 20 } 21 finally 22 { 23 } 24 return boxed.m_value; 25 } 26 27 private Boxed CreateValue() 28 { 29 Boxed boxed = null; 30 if (m_valueFactory != null) //() => func(arg) 31 { 32 try 33 { 34 Func<T> factory = m_valueFactory; 35 36 boxed = new Boxed(factory()); 37 } 38 catch (Exception ex) 39 { 40 throw; 41 } 42 } 43 44 45 return boxed; 46 } 47 48 [Serializable] 49 class Boxed 50 { 51 internal Boxed(T value) 52 { 53 m_value = value; 54 } 55 internal T m_value; 56 }
如今咱們看下改進方法:
1 /// <summary> 2 /// 爲解決GetOrAdd 非原子性操做, 3 /// 重複初始化操做,引入Lazy類型、 4 /// 延遲初始化 5 /// </summary> 6 /// <typeparam name="T"></typeparam> 7 /// <typeparam name="R"></typeparam> 8 /// <param name="func"></param> 9 /// 使用延遲初始化來延遲建立大型或消耗大量資源的對象,或者執行大量佔用資源的任務 10 /// ,尤爲是在程序的生存期內可能不會發生這種建立或執行時。 11 /// 若要爲遲緩初始化作好準備,請建立的實例 Lazy<T> 。 12 /// 你建立的對象的類型參數 Lazy<T> 指定你但願延遲初始化的對象的類型。 13 /// 用於建立對象的構造函數 Lazy<T> 肯定初始化的特徵。 14 /// 首次訪問 Lazy<T>.Value 屬性時出現延遲初始化。 15 /// <returns></returns> 16 public static Func<T, R> MemoizeLazyThreadSafe<T, R>(Func<T, R> func) where T : IComparable 17 { 18 ConcurrentDictionary<T, Lazy<R>> cache = new ConcurrentDictionary<T, Lazy<R>>(); 19 return arg => 20 { 21 return cache.GetOrAdd(arg, a => new Lazy<R>(() => func(arg))).Value; 22 }; 23 }
到如今方法的線程安全、初始化加載問題都解決了,可是咱們在解決重複計算的問題後卻又不得不考慮緩存帶來的內存損耗問題。咱們實例化了ConcurrentDictionary對象,而且該對象做爲強引用類型一直未被釋放,那麼GC是沒法回收該對象,帶來的問題是內存一直被佔用,隨着方法引用次數愈來愈多內存開銷則會愈來愈大。
四、爲解決該問題,咱們引入過時時間,根據過時時間釋放緩存值。
1 public static Func<T, R> MemoizeWeakWithTtl<T, R>(Func<T, R> func, TimeSpan ttl) 2 where T : class, IEquatable<T> 3 where R : class 4 { 5 var keyStore = new ConcurrentDictionary<int, T>(); 6 7 T ReduceKey(T obj) 8 { 9 var oldObj = keyStore.GetOrAdd(obj.GetHashCode(), obj); 10 return obj.Equals(oldObj) ? oldObj : obj; 11 } 12 13 var cache = new ConditionalWeakTable<T, Tuple<R, DateTime>>(); 14 15 Tuple<R, DateTime> FactoryFunc(T key) => 16 new Tuple<R, DateTime>(func(key), DateTime.Now + ttl); 17 18 return arg => 19 { 20 var key = ReduceKey(arg); 21 var value = cache.GetValue(key, FactoryFunc); 22 if (value.Item2 >= DateTime.Now) 23 return value.Item1; 24 value = FactoryFunc(key); 25 cache.Remove(key); 26 cache.Add(key, value); 27 return value.Item1; 28 }; 29 }
其餘實現方式,使用WeakReference弱引用類型(如下爲使用示例):
1 public class Cache 2 { 3 static Dictionary<int, WeakReference> _cache; 4 5 int regenCount = 0; 6 7 public Cache(int count) 8 { 9 _cache = new Dictionary<int, WeakReference>(); 10 11 for (int i = 0; i < count; i++) 12 { 13 _cache.Add(i, new WeakReference(new Data(i), false)); 14 } 15 } 16 17 public int Count 18 { 19 get { return _cache.Count; } 20 } 21 22 public int RegenerationCount 23 { 24 get { return regenCount; } 25 } 26 27 public Data this[int index] 28 { 29 get 30 { 31 Data d = _cache[index].Target as Data; 32 if (d == null) 33 { 34 Console.WriteLine("Regenerate object at {0}: Yes", index); 35 d = new Data(index); 36 _cache[index].Target = d; 37 regenCount++; 38 } 39 else 40 { 41 Console.WriteLine("Regenerate object at {0}: No", index); 42 } 43 44 return d; 45 } 46 } 47 } 48 49 50 public class Data 51 { 52 private byte[] _data; 53 private string _name; 54 55 public Data(int size) 56 { 57 _data = new byte[size * 1024]; 58 _name = size.ToString(); 59 } 60 61 // Simple property. 62 public string Name 63 { 64 get { return _name; } 65 } 66 }
1 int cacheSize = 50; 2 Random r = new Random(); 3 Cache c = new Cache(cacheSize); 4 5 string DataName = ""; 6 GC.Collect(0); 7 8 for (int i = 0; i < c.Count; i++) 9 { 10 int index = r.Next(c.Count); 11 DataName = c[index].Name; 12 } 13 double regenPercent = c.RegenerationCount / (double)c.Count; 14 Console.WriteLine("Cache size: {0}, Regenerated: {1:P2}%", c.Count, regenPercent);
打印結果:
1 Regenerate object at 46: Yes 2 Regenerate object at 5: Yes 3 Regenerate object at 6: Yes 4 Regenerate object at 31: Yes 5 Regenerate object at 1: Yes 6 Regenerate object at 33: Yes 7 Regenerate object at 11: Yes 8 Regenerate object at 5: No 9 Regenerate object at 37: Yes 10 Regenerate object at 15: Yes 11 Regenerate object at 25: Yes 12 Regenerate object at 14: No 13 Regenerate object at 16: Yes 14 Regenerate object at 20: Yes 15 Regenerate object at 10: Yes 16 Regenerate object at 14: No 17 Regenerate object at 17: Yes 18 Regenerate object at 28: Yes 19 Regenerate object at 7: Yes 20 Regenerate object at 34: Yes 21 Regenerate object at 45: Yes 22 Regenerate object at 33: No 23 Regenerate object at 29: Yes 24 Regenerate object at 32: Yes 25 Regenerate object at 32: No 26 Regenerate object at 4: No 27 Regenerate object at 42: Yes 28 Regenerate object at 6: No 29 Regenerate object at 16: No 30 Regenerate object at 36: Yes 31 Regenerate object at 12: Yes 32 Regenerate object at 9: Yes 33 Regenerate object at 43: Yes 34 Regenerate object at 12: No 35 Regenerate object at 49: Yes 36 Regenerate object at 37: No 37 Regenerate object at 36: No 38 Regenerate object at 44: Yes 39 Regenerate object at 22: Yes 40 Regenerate object at 31: No 41 Regenerate object at 1: No 42 Regenerate object at 24: No 43 Regenerate object at 23: Yes 44 Regenerate object at 38: Yes 45 Regenerate object at 6: No 46 Regenerate object at 31: No 47 Regenerate object at 28: No 48 Cache size: 50, Regenerated: 66.00%%
具體實現方式不在此實現。