如何使这个Concurrent Dictionary与计时器一起过期?

时间:2016-08-30 16:45:17

标签: c# caching concurrentdictionary

这段代码似乎可以很好地缓存异步方法的结果。我想为它添加一些过期。我已经尝试了Tuple,但是我没有成功地完成工作/编译。

private static readonly ConcurrentDictionary<object, SemaphoreSlim> _keyLocks = new ConcurrentDictionary<object, SemaphoreSlim>();
    private static readonly ConcurrentDictionary<object, Tuple<List<UnitDTO>, DateTime>> _cache = new ConcurrentDictionary<object, Tuple<List<UnitDTO>, DateTime>>();

public async Task<string> GetSomethingAsync(string key)
{   
    string value;
    // get the semaphore specific to this key
    var keyLock = _keyLocks.GetOrAdd(key, x => new SemaphoreSlim(1));
    await keyLock.WaitAsync();
    try
    {
        // try to get value from cache
        if (!_cache.TryGetValue(key, out value))
        {
            // if value isn't cached, get it the long way asynchronously
            value = await GetSomethingTheLongWayAsync();

            // cache value
            _cache.TryAdd(key, value);
        }
    }
    finally
    {
        keyLock.Release();
    }
    return value;
}

1 个答案:

答案 0 :(得分:1)

经典方法和引文

来自msdn,来自Stephen Cleary

  

异步代码通常用于初始化当时的资源   缓存和共享。没有内置类型,但斯蒂芬   Toub开发了一个AsyncLazy,就像Task和   懒。原始类型在他的blog上描述,并且是   我的AsyncEx library中提供了更新版本。

public class AsyncLazy<T> : Lazy<Task<T>> 
{ 
    public AsyncLazy(Func<T> valueFactory) : 
        base(() => Task.Factory.StartNew(valueFactory)) { }
    public AsyncLazy(Func<Task<T>> taskFactory) : 
        base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap()) { } 
}

<强>上下文

假设我们的程序中有一个AsyncLazy实例:

static string LoadString() { … }
static AsyncLazy<string> m_data = new AsyncLazy<string>(LoadString);

<强>用法

因此,我们可以编写一个执行以下操作的异步方法:

string data = await m_data.Value;

Lazy<T>是合适的,但很遗憾它似乎缺少输入参数来索引结果。同样的问题已解决here,其中解释了如何缓存长期运行的资源密集型方法的结果,以防它 async

返回您提出的解决方案

在我展示与缓存管理相关的主要更改并且特定于您提议的实施之前,让我建议一些边缘优化选项,基于以下{ {3}}

  

经常使用锁,当你访问它们时,它们是无竞争的,并且在   在这种情况下,你真的想要获取并释放锁定为   尽可能低开销;换句话说,访问无竞争锁   应该涉及一条快速路径

由于他们只是性能优化技巧,我会在代码中对其进行评论,以便您可以衡量在特定情况下的效果

  1. 您需要在等待之后再次测试TryGetValue,因为另一个并行进程可能在此期间添加了该值
  2. 在您等待
  3. 时,您不需要保持锁定

    这个开销与缓存未命中的平衡已经在之前的concerns指出了类似的问题。

      

    显然,开销保持SemaphoreSlim对象的存在   防止缓存未命中,因此根据用途可能不值得   案件。但是,如果保证没有缓存未命中比这更重要   完成了。

    我的主要答案:缓存管理

    关于缓存过期,我建议将创建DateTime添加到Dictionary的值(即从GetSomethingTheLongWayAsync返回值的时间),并因此在固定的时间跨度后丢弃缓存的值。

    查找以下草稿

        private static readonly ConcurrentDictionary<object, SemaphoreSlim> _keyLocks = new ConcurrentDictionary<object, SemaphoreSlim>();
        private static readonly ConcurrentDictionary<object, Tuple<string, DateTime>> _cache = new ConcurrentDictionary<object, Tuple<string, DateTime>>();
    
    
        private static bool IsExpiredDelete(Tuple<string, DateTime> value, string key)
        {
            bool _is_exp = (DateTime.Now - value.Item2).TotalMinutes > Expiration;
            if (_is_exp)
            {
                _cache.TryRemove(key, out value);
            }
            return _is_exp;
        }
        public async Task<string> GetSomethingAsync(string key)
        {
            Tuple<string, DateTime> cached;
            // get the semaphore specific to this key
            var keyLock = _keyLocks.GetOrAdd(key, x => new SemaphoreSlim(1));
            await keyLock.WaitAsync();
            try
            {
                // try to get value from cache
                if (!_cache.TryGetValue(key, out cached) || IsExpiredDelete(cached,key))
                {
                    //possible performance optimization: measure it before uncommenting
                    //keyLock.Release();
                    string value = await GetSomethingTheLongWayAsync(key);
                    DateTime creation = DateTime.Now; 
                    // in case of performance optimization
                    // get the semaphore specific to this key
                    //keyLock = _keyLocks.GetOrAdd(key, x => new SemaphoreSlim(1));
                    //await keyLock.WaitAsync();
                    bool notFound;
                    if (notFound = !_cache.TryGetValue(key, out cached) || IsExpiredDelete(cached, key))
                    {
                        cached = new Tuple<string, DateTime>(value, creation);
                        _cache.TryAdd(key, cached);
                    }
                    else
                    {
                        if (!notFound && cached.Item2 < creation)
                        {
                            cached = new Tuple<string, DateTime>(value, creation);
                        _cache.TryAdd(key, cached);
                        }
                    }
                }
            }
            finally
            {
                keyLock.Release();
            }
            return cached?.Item1;
        }
    

    请根据您的具体需求调整上述代码。

    使其更通用

    最后你可能想稍微概括一下。

    顺便提一下,请注意Dictionary static,因为可以使用相同的<来缓存两个不同的方法/ em>签名。

    public class Cached<FromT, ToT>
    {
        private Func<FromT, Task<ToT>> GetSomethingTheLongWayAsync;
        public Cached (Func<FromT, Task<ToT>> _GetSomethingTheLongWayAsync, int expiration_min ) {
            GetSomethingTheLongWayAsync = _GetSomethingTheLongWayAsync;
            Expiration = expiration_min;
    }
    
        int Expiration = 1;
    
        private ConcurrentDictionary<FromT, SemaphoreSlim> _keyLocks = new ConcurrentDictionary<FromT, SemaphoreSlim>();
        private ConcurrentDictionary<FromT, Tuple<ToT, DateTime>> _cache = new ConcurrentDictionary<FromT, Tuple<ToT, DateTime>>();
    
    
        private bool IsExpiredDelete(Tuple<ToT, DateTime> value, FromT key)
        {
            bool _is_exp = (DateTime.Now - value.Item2).TotalMinutes > Expiration;
            if (_is_exp)
            {
                _cache.TryRemove(key, out value);
            }
            return _is_exp;
        }
        public async Task<ToT> GetSomethingAsync(FromT key)
        {
            Tuple<ToT, DateTime> cached;
            // get the semaphore specific to this key
            var keyLock = _keyLocks.GetOrAdd(key, x => new SemaphoreSlim(1));
            await keyLock.WaitAsync();
            try
            {
                // try to get value from cache
                if (!_cache.TryGetValue(key, out cached) || IsExpiredDelete(cached, key))
                {
                    //possible performance optimization: measure it before uncommenting
                    //keyLock.Release();
                    ToT value = await GetSomethingTheLongWayAsync(key);
                    DateTime creation = DateTime.Now;
                    // in case of performance optimization
                    // get the semaphore specific to this key
                    //keyLock = _keyLocks.GetOrAdd(key, x => new SemaphoreSlim(1));
                    //await keyLock.WaitAsync();
                    bool notFound;
                    if (notFound = !_cache.TryGetValue(key, out cached) || IsExpiredDelete(cached, key))
                    {
                        cached = new Tuple<ToT, DateTime>(value, creation);
                        _cache.TryAdd(key, cached);
                    }
                    else
                    {
                        if (!notFound && cached.Item2 < creation)
                        {
                            cached = new Tuple<ToT, DateTime>(value, creation);
                            _cache.TryAdd(key, cached);
                        }
                    }
                }
            }
            finally
            {
                keyLock.Release();
            }
            return cached.Item1;
        }
    
    }
    

    对于通用FromTIEqualityComparer需要Dictionary

    <强>用法/演示

        private static async Task<string> GetSomethingTheLongWayAsync(int key)
        {
            await Task.Delay(15000);
            Console.WriteLine("Long way for: " + key);
            return key.ToString();
        }
    
        static void Main(string[] args)
        {
            Test().Wait();
        }
    
        private static async Task Test()
        {
            int key;
            string val;
            key = 1;
            var cache = new Cached<int, string>(GetSomethingTheLongWayAsync, 1);
            Console.WriteLine("getting " + key);
            val = await cache.GetSomethingAsync(key);
            Console.WriteLine("getting " + key + " resulted in " + val);
    
            Console.WriteLine("getting " + key);
            val = await cache.GetSomethingAsync(key);
            Console.WriteLine("getting " + key + " resulted in " + val);
    
            await Task.Delay(65000);
    
            Console.WriteLine("getting " + key);
            val = await cache.GetSomethingAsync(key);
            Console.WriteLine("getting " + key + " resulted in " + val);
            Console.ReadKey();
        }
    

    复杂的替代方案

    还有更高级的可能性,例如GetOrAdd的重载,它接受委托和Lazy对象,以确保只调用一次生成器函数(而不是信号量和锁)。

       public class AsyncCache<FromT, ToT>
        {
            private Func<FromT, Task<ToT>> GetSomethingTheLongWayAsync;
            public AsyncCache(Func<FromT, Task<ToT>> _GetSomethingTheLongWayAsync, int expiration_min)
            {
                GetSomethingTheLongWayAsync = _GetSomethingTheLongWayAsync;
                Expiration = expiration_min;
            }
    
            int Expiration;
    
            private ConcurrentDictionary<FromT, Tuple<Lazy<Task<ToT>>, DateTime>> _cache = 
                new ConcurrentDictionary<FromT, Tuple<Lazy<Task<ToT>>, DateTime>>();
    
    
            private bool IsExpiredDelete(Tuple<Lazy<Task<ToT>>, DateTime> value, FromT key)
            {
                bool _is_exp = (DateTime.Now - value.Item2).TotalMinutes > Expiration;
                if (_is_exp)
                {
                    _cache.TryRemove(key, out value);
                }
                return _is_exp;
            }
            public async Task<ToT> GetSomethingAsync(FromT key)
            {
                var res = _cache.AddOrUpdate(key,
                    t =>  new Tuple<Lazy<Task<ToT>>, DateTime>(new Lazy<Task<ToT>>(
                          () => GetSomethingTheLongWayAsync(key)
                        )
                    , DateTime.Now) ,
                    (k,t) =>
                    {
                        if (IsExpiredDelete(t, k))
                        {
                            return new Tuple<Lazy<Task<ToT>>, DateTime>(new Lazy<Task<ToT>>(
                          () => GetSomethingTheLongWayAsync(k)
                        ), DateTime.Now);
                        }
                        return t;
                    }
    
                    );
                return await res.Item1.Value;
            }
    
        }
    

    相同用法,只需替换AsyncCache而不是Cached