【问题标题】:Lazy caching of a method (like a DB getter)方法的延迟缓存(如 DB getter)
【发布时间】:2016-08-12 18:46:00
【问题描述】:

只是为了避免重新发明轮子 我想知道是否已经存在标准 C# 实现来缓存来自长期运行、资源密集型方法的结果。 在我看来,Lazy<T> 是合适的,但不幸的是它似乎缺少索引结果的输入参数。 我希望以下内容有助于澄清:这是我的自定义解决方案。

public class Cached<FromT,ToT>
{
    private Func<FromT,ToT> _my_func;
    private Dictionary<FromT,ToT> funcDict;
    public Cached(Func<FromT,ToT> coreFunc, IEqualityComparer<FromT> comparer = null)
    {
        _my_func = coreFunc;
        if (comparer != null) {
            funcDict = new Dictionary<FromT,ToT>(comparer);
        } else {
            funcDict = new Dictionary<FromT,ToT>();
        }
    }
    public ToT Get(FromT fromKey) {
        if (!funcDict.ContainsKey(fromKey)) {
            funcDict.Add(fromKey, _my_func(fromKey) );
        }
        return funcDict[fromKey];
    }
}

在下面找到我的单元测试代码。

string DBSimulation(int example, bool quick = false) {
    if (!quick) Thread.Sleep(15000);
    return example.ToString();
}

[Test]
public void Test03Cached() {
    var testCache = new Functional.Cached<int,string>(x => DBSimulation(x));
    DateTime checkNow = DateTime.Now;
    string logResult = "";
    for (int i = 0; i < 24; i++) {
        Assert.AreEqual(DBSimulation( i % 3, true), testCache.Get( i % 3));
        logResult += String.Format("After {0} seconds => {1} returned from {2} \n",
                                   ((TimeSpan)(DateTime.Now - checkNow)).TotalSeconds,
                                   testCache.Get( i % 3), i);
    }
    Console.WriteLine(logResult);
    double elapsed = ((TimeSpan)(DateTime.Now - checkNow)).TotalSeconds;
    Assert.LessOrEqual(elapsed, 15*3+1,"cache not working, {0} seconds elapsed", elapsed);
}

及其输出

After 15,0035002 seconds => 1 returned from 1 
After 30,0050002 seconds => 2 returned from 2 
After 45,0065002 seconds => 0 returned from 3 
After 45,0065002 seconds => 1 returned from 4 
After 45,0065002 seconds => 2 returned from 5 
... 
After 45,0065002 seconds => 0 returned from 21 
After 45,0065002 seconds => 1 returned from 22 
After 45,0065002 seconds => 2 returned from 23 

编辑

对于通用 FromT,字典需要 IEqualityComparer

【问题讨论】:

    标签: c# caching lazy-evaluation


    【解决方案1】:

    是的,不错的解决方案,但我为您提供了一些关于并发性的改进 (ConcurrentDictionary)。例如,您可以在许多线程可以同时使用此类的 asp.net 应用程序中使用此代码。此外,由于此类将用于长时间运行的函数,因此在函数完成时不等待结果而是稍后处理结果(Task.ContinueWith)将是一个很好的特性:

    public class Cached<FromT, ToT>
    {
        private Func<FromT, ToT> _my_func;
        private ConcurrentDictionary<FromT, ToT> funcDict = new ConcurrentDictionary<FromT, ToT>();
        private Random rand = new Random();
    
        public Cached(Func<FromT, ToT> coreFunc)
        {
            _my_func = coreFunc;
        }
    
        public Task<ToT> Get(FromT fromKey)
        {
            if (!funcDict.ContainsKey(fromKey))
                return Task.Factory.StartNew(() => {
                    var result = _my_func(fromKey);
                    do
                    {
                        if (!funcDict.ContainsKey(fromKey) && !funcDict.TryAdd(fromKey, result))                        
                            Thread.Sleep(rand.Next(50, 100));                                                    
                        else
                            break;
                    } while (true);                    
                    return result;
                });
            ToT answer;
            funcDict.TryGetValue(fromKey, out answer);
            return Task.FromResult(answer);
        }
    }
    
    public static string DBSimulation(int example)
    {
        Thread.Sleep(15000);
        return example.ToString();
    }
    
    public static void Main()
    {
        var testCache = new Cached<int, string>(x => DBSimulation(x));
        DateTime checkNow = DateTime.Now;
        for (int i = 0; i < 24; i++)
        {
            var j = i;
            testCache.Get(i % 3).ContinueWith(x => 
                Console.WriteLine(String.Format("After {0} seconds => {1} returned from {2}",
                                               ((TimeSpan)(DateTime.Now - checkNow)).TotalSeconds,
                                               x.Result, j)));
        }            
        Console.ReadKey();        
    }
    

    结果:

    After 15.0164309 seconds => 0 returned from 6
    After 15.0164309 seconds => 2 returned from 5
    After 15.0164309 seconds => 1 returned from 4
    After 15.0164309 seconds => 0 returned from 3
    After 15.0164309 seconds => 0 returned from 0
    ......
    After 26.5133477 seconds => 1 returned from 19
    After 27.5112726 seconds => 2 returned from 20
    After 28.5127277 seconds => 0 returned from 21
    After 29.5126096 seconds => 1 returned from 22
    After 30.0204739 seconds => 2 returned from 23
    

    附:如您所见,执行所有操作的时间减少了 从 45 秒到 30 秒,而不是 15 秒,因为这取决于 核心,启动线程数和其他东西,还有一些时间是 用于字典的同步。

    【讨论】:

    • 谢谢,我将使用您的代码(我想更好地理解时间减少,但是由于并行执行,这是合理的)。只是一个小提示,我已经从字典中删除了 static 关键字,因为我可以有两个具有相同签名的不同函数,返回不同的结果以分别缓存
    • 好的,我了解你关于静态关键字并修改了答案。
    • 谢谢,+1 并接受。仅供参考,出于好奇,用 FromResult 替换 Task.FromResult 时,在旧的 .NET 4.0 中似乎也可以正常工作
    • 一些进一步的cmets。 1) 并发解决方案在第一轮不缓存任何内容(所有 for 迭代都以空字典开始),它仅从第二次执行开始缓存。 2)我的原始代码中有一个非常严重的问题,因为对于通用 FromT,字典需要一个 IEqualityComparer ...我将解决这个问题,然后我将编辑我的问题,以供将来参考 3)在结论答案是说没有标准解决方案,必须重新实施
    • 关于 1) 和 2) 发生这种情况是因为所有线程都立即启动并且所有线程都尝试一次将函数的结果写入字典,并且由于本机字典的锁定而没有人可以执行它,并且只能在由于失去同步,他们可以进行第二次迭代。我已经修改了我的代码来解决这个问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-02-17
    • 2019-05-17
    • 2012-08-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多