函数式编程-记忆化缓存

记忆化,是一种为了提高应用程序性能的FP技术。程序加速是通过缓存函数的结果实现的,避免了重复计算带来的额外开销。

1、现在我们使用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,1)">();
 5     return arg =>
 6     {
 7         if (cache.ContainsKey(arg))
 8             return cache[arg];
 9         return (cache[arg] = func(arg));
10     };
11 }
1 static string GetString(string name)
2 3     return $"return date {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss)} string {name}";
4 }
var getStrMemoize = Memoize<string,string>(GetString);
2 Console.WriteLine(getStrMemoize(A));
3 Thread.Sleep(3000);
4 Console.WriteLine(getStrMemoize(B5 Thread.Sleep(6 Console.WriteLine(getStrMemoize("));

打印结果:

return date 2020-12-31 08:37:12  A
15  B
3 string A

可以看出第三次打印的结果跟第一次打印的结果相同,也就是被缓存在Dictionary中的值。

在单线程中我们这样写没有问题,程序顺序被执行,Dictionary不存在并发问题,但是当我们想在多个线程并行时Dictionary不是线程安全集合,会存在线程安全问题。

2、现在我们使用线程安全集合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 类上的所有其他操作而言,不是原子的 ConcurrentDictionary10  由于在生成值时,另一个线程可以插入键/值 valueFactory ,因此您不能信任这一点,
11  因为已 valueFactory 执行,其生成的值将插入到字典中并返回。
12  如果 GetOrAdd 在不同的线程上同时调用,则 valueFactory 可以多次调用,但只会将一个键/值对添加到字典中。
13  返回值取决于字典中的键是否存在,以及是否在 GetOrAdd 调用之后但在生成值之前由另一个线程插入了键/值 valueFactory
14  (如果当前线程检查到Key不在字典中,那么会执行生成键值;但是在写入前如果有线程完成了写入键值,当前线程写入前检查到有写入值,则以已写入的为准)。
15 <returns></returns>
16 17 18     ConcurrentDictionary<T,1)">new ConcurrentDictionary<T,1)">19     20 21         return cache.GetOrAdd(arg,a =>22 23 }
var getStrMemoize = MemoizeThreadSafe<42:46 49 string A

注解中我们说明了ConcurrentDictionary是线程安全集合,但是当我们使用GetOrAdd时,由于该方法不是原子性的操作,当进行初始化时,可能多个线程同时进行初始化操作,带来了额外的开销。

3、为解决GetOrAdd非原子性操作重复初始化操作,引入延迟初始化(注解已详细说明):

在看改进方法前我们先看下Lazy类的用法:

class user
string name { get; set; }
1 Lazy<user> user = new Lazy<user>if (!user.IsValueCreated)
3     Console.WriteLine(user 未创建.4 user.Value.name = test5  (user.IsValueCreated)
6     Console.WriteLine(user 已创建.");

输出:

user 未创建.
2 user 已创建.

以下为Lazy类代码片段,从代码我们看出在对象未使用(value)前,实例并未真正创建:

[NonSerialized]
 2 private Func<T> m_valueFactory;
 3 
 4 private object m_boxed;
 5 
public T Value
 7  8     get
 9 10          LazyInitValue();
11     }
}
13 private T LazyInitValue()
14 15     Boxed boxed = null16     try
18         boxed = CreateValue();
19         m_boxed = boxed;
21     finally
23 24      boxed.m_value;
25 26 
27  Boxed CreateValue()
28 29     Boxed boxed = 30     if (m_valueFactory != null) //() => func(arg)
31 32         33         {
34             Func<T> factory =35 
36             boxed = new Boxed(factory());
37         }
38         catch (Exception ex)
39 40             throw41 42 43 
44 
45     47 
48 [Serializable]
 Boxed
50 51     internal Boxed(T value)
52 53         m_value = value;
54 55      T m_value;
56 }

现在我们看下改进方法:

 为解决GetOrAdd 非原子性操作,
 重复初始化操作,引入Lazy类型、
 延迟初始化
 使用延迟初始化来延迟创建大型或消耗大量资源的对象,或者执行大量占用资源的任务
 ,尤其是在程序的生存期内可能不会发生这种创建或执行时。
 若要为迟缓初始化做好准备,请创建的实例 Lazy<T> 你创建的对象的类型参数 Lazy 指定你希望延迟初始化的对象的类型。
 用于创建对象的构造函数 Lazy 确定初始化的特征。
 首次访问 Lazy.Value 属性时出现延迟初始化。
18   ConcurrentDictionary<T,Lazy<R>> cache = 19     {
21       new Lazy<R>(() => func(arg))).Value;
  };
23 }

到现在方法的线程安全、初始化加载问题都解决了,但是我们在解决重复计算的问题后却又不得不考虑缓存带来的内存损耗问题。我们实例化了ConcurrentDictionary对象,并且该对象作为强引用类型一直未被释放,那么GC是无法回收该对象,带来的问题是内存一直被占用,随着方法引用次数越来越多内存开销则会越来越大。

4、为解决该问题,我们引入过期时间,根据过期时间释放缓存值。

 func,TimeSpan ttl)
where T : class,IEquatable<T>
 3     where R : class
var keyStore = new ConcurrentDictionary<int,T> 6 
    T ReduceKey(T obj)
 8 var oldObj = keyStore.GetOrAdd(obj.GetHashCode(),obj);
return obj.Equals(oldObj) ? oldObj : obj;
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     19 20         var key = ReduceKey(arg);
var value = cache.GetValue(key,FactoryFunc);
22         if (value.Item2 >= DateTime.Now)
23              value.Item1;
24         value = FactoryFunc(key);
        cache.Remove(key);
26         cache.Add(key,value);
27         29 }

其他实现方式,使用WeakReference弱引用类型(以下为使用示例):

 Cache
static Dictionary< _cache;
 4 
int regenCount = 0 7     public Cache(int count)
 9         _cache = new Dictionary<10 
11         for (int i = 0; i < count; i++)
13             _cache.Add(i,1)">new WeakReference(new Data(i),1)">false16 
17      Count
18 19         get {  _cache.Count; }
21 
22      RegenerationCount
24          regenCount; }
27     public Data this[ index]
29         30 31             Data d = _cache[index].Target as Data;
32             if (d ==             {
34                 Console.WriteLine(Regenerate object at {0}: Yes,index);
35                 d =  Data(index);
36                 _cache[index].Target = d;
37                 regenCount++38             }
39             else
40 41                 Console.WriteLine(Regenerate object at {0}: No44             45 47 48 
49 
 Data
51 52     byte[] _data;
53      _name;
54 
public Data( size)
56 57         _data = new byte[size * 1024];
58         _name = size.ToString();
59 60 
61      Simple property.
62      Name
63 64          _name; }
65 66 }
int cacheSize = 50 2 Random r =  Random();
 3 Cache c =  Cache(cacheSize);
 5 string DataName = "" 6 GC.Collect( 7 
0; i < c.Count; i++10     int index = r.Next(c.Count);
11     DataName = c[index].Name;
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 5 3 Regenerate 6 4 Regenerate 31 5 Regenerate 1 6 Regenerate 33 7 Regenerate 11 8 Regenerate : No
 9 Regenerate 3710 Regenerate 1511 Regenerate 2512 Regenerate 1413 Regenerate 1614 Regenerate 2015 Regenerate 1016 Regenerate 17 Regenerate 1718 Regenerate 2819 Regenerate 720 Regenerate 3421 Regenerate 4522 Regenerate 23 Regenerate 2924 Regenerate 3225 Regenerate 26 Regenerate 427 Regenerate 4228 Regenerate 29 Regenerate 30 Regenerate 3631 Regenerate 1232 Regenerate 933 Regenerate 4334 Regenerate 35 Regenerate 4936 Regenerate 37 Regenerate 38 Regenerate 4439 Regenerate 2240 Regenerate 41 Regenerate 42 Regenerate 2443 Regenerate 2344 Regenerate 3845 Regenerate 46 Regenerate 47 Regenerate 48 Cache size: 50,Regenerated: 66.00%%

具体实现方式不在此实现。

 

相关文章

项目中经常遇到CSV文件的读写需求,其中的难点主要是CSV文件...
简介 本文的初衷是希望帮助那些有其它平台视觉算法开发经验的...
这篇文章主要简单记录一下C#项目的dll文件管理方法,以便后期...
在C#中的使用JSON序列化及反序列化时,推荐使用Json.NET——...
事件总线是对发布-订阅模式的一种实现,是一种集中式事件处理...
通用翻译API的HTTPS 地址为https://fanyi-api.baidu.com/api...