如何使用计时器使此并发字典过期?

本文关键字:字典 过期 并发 何使用 计时器 | 更新日期: 2023-09-27 18:04:41

这段代码似乎很好地缓存了异步方法的结果。我想给它加上一些有效期。我已经尝试过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;
}

如何使用计时器使此并发字典过期?

经典方法及引语

From msdn, by Stephen Cleary

异步代码通常用于初始化当前的资源缓存和共享。没有内置的类型,但是StephenToub开发了一个asynlazy,它的作用类似于Task和懒惰。他的博客上描述了最初的类型更新的版本在我的AsyncEx库中可用。
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()) { } 
}
<<p> 上下文/strong>

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

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

因此,我们可以编写一个异步方法来完成:
string data = await m_data.Value;

Lazy<T>是合适的,但是不幸的是似乎缺少索引结果的输入参数。这里解决了同样的问题,解释了如何缓存来自长时间运行的资源密集型方法的结果,如果它是而不是 async

回到你提出的解决方案

在我展示与缓存管理相关的主要更改和特定于您提议的实现之前,让我基于以下关注点建议几个边缘优化选项

通常与锁一起使用,当您访问它们时,它们是非争用的在这种情况下,您确实希望获取和释放锁是相同的尽可能降低开销;换句话说,访问非争用锁应该包含一个快速路径

由于它们只是性能优化技巧,我将在代码中注释它们,以便您可以在之前在您的特定情况下测量它们的效果。

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

这个开销与缓存丢失的平衡已经在之前的一个类似问题的回答中指出了。

显然,有开销保持SemaphoreSlim对象防止缓存丢失,因此根据使用情况,这样做可能不值得的情况。但如果保证没有缓存丢失比这更重要完成。

我的主要回答:缓存管理

关于缓存过期,我建议将创建日期时间添加到字典的值(即从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,因为可以用相同的签名缓存两个不同的方法。
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;
    }
}

对于通用的FromT, Dictionary需要IEqualityComparer

<<p> 使用/演示/strong>
    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