我怎样才能让这个并发字典用计时器过期?

How can I make this Concurrent Dictionary expire with a timer?

这段代码似乎在缓存异步方法结果方面做得很好。我想给它添加某种到期时间。我已经尝试过 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;
}

经典方法和引语

来自 msdn,作者:Stephen Cleary

Asynchronous code is often used to initialize a resource that’s then cached and shared. There isn’t a built-in type for this, but Stephen Toub developed an AsyncLazy that acts like a merge of Task and Lazy. The original type is described on his blog, and an updated version is available in my 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> 是合适的,但不幸的是 它似乎缺少索引结果的输入参数。同样的问题已解决 ,其中解释了如何缓存长 运行、资源密集型方法的结果,以防 不是 异步

回到您提出的解决方案

在我展示与缓存管理相关的主要更改以及具体到您提议的实施之前,让我建议一些 marginal 优化 options,基于以下 concerns.

often with locks, when you access them they’re uncontended, and in such cases you really want acquiring and releasing the lock to be as low-overhead as possible; in other words, accessing uncontended locks should involve a fast path

因为它们只是性能优化技巧,我会在代码中对它们进行注释,以便您可以衡量它们的效果你的具体情况之前.

  1. 您需要在等待后再次测试 TryGetValue,因为另一个并行进程可能同时添加了该值
  2. 您在等待时无需保留锁

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

Obviously, there's overhead keeping SemaphoreSlim objects around to prevent cache misses so it may not be worth it depending on the use case. But if guaranteeing no cache misses is important than this accomplishes that.

我的主要回答:缓存管理

关于缓存过期,我建议将创建日期时间添加到字典的值(即从 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

Usage/Demo

    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