Lazy caching of a method (like a DB getter)

2019-09-05 17:16发布

Just to avoid reinventing the wheel I'm wondering whether a standard C# implementation already exists to cache the results from a long-running, resource-intensive method. To my mind, the Lazy<T> would be appropriate, but unfortunately it seems to lack the input parameter to index the result. I hope the following helps to clarify: this is my custom solution.

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];
    }
}

Find below my unit-testing code.

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);
}

with its output

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 

Edit

For a generic FromT an IEqualityComparer is needed for the Dictionary

1条回答
走好不送
2楼-- · 2019-09-05 17:33

Yes, not bad solution, but I offer you some improvements on concurrency(ConcurrentDictionary). You can use this code, for example, at your asp.net application where many threads simultaneously can use this class. Also as this class will be used for longrunning functions it will be good feature to not wait result but instead process result later(Task.ContinueWith) when function will be completed:

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();        
}

RESULT:

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

P.S. as you can see time elapsed to perform all operations reduced from 45 to 30 seconds, not to 15, because it depends on number of cores, number of started threads and other stuff, also some of time is expended to dictionary's synchronization.

查看更多
登录 后发表回答