【问题标题】:Object cache for C#C# 的对象缓存
【发布时间】:2010-10-09 12:41:55
【问题描述】:

我正在为某些文档格式做一个文档查看器。为方便起见,假设这是一个 PDF 查看器,一个桌面应用程序。对软件的一项要求是渲染速度。所以,现在,当用户滚动浏览文档时,我正在为下一页缓存图像。

这行得通,UI 响应速度非常快,看起来应用程序几乎可以立即呈现页面......但代价是:内存使用量有时会达到 600MB。我把它全部缓存在内存中。

现在,我可以缓存到磁盘,我知道,但是一直这样做会明显变慢。我想做的是实现一些缓存(LRU?),其中一些缓存页面(图像对象)在内存上,其中大部分在磁盘上。

在我开始这个之前,框架或某个库中有什么东西可以为我做这件事吗?这似乎是一个相当普遍的问题。 (这是一个桌面应用程序,不是 ASP.NET)

或者,您对这个问题还有其他想法吗?

【问题讨论】:

    标签: c# .net caching


    【解决方案1】:

    我写了一个 LRU Cache 和一些测试用例,请随意使用。

    您可以在my blog 上阅读源代码。

    对于懒惰的人(这里是减去测试用例):

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace LRUCache {
        public class IndexedLinkedList<T> {
    
            LinkedList<T> data = new LinkedList<T>();
            Dictionary<T, LinkedListNode<T>> index = new Dictionary<T, LinkedListNode<T>>();
    
            public void Add(T value) {
                index[value] = data.AddLast(value);
            }
    
            public void RemoveFirst() {
                index.Remove(data.First.Value);
                data.RemoveFirst();
            }
    
            public void Remove(T value) {
                LinkedListNode<T> node;
                if (index.TryGetValue(value, out node)) {
                    data.Remove(node);
                    index.Remove(value);
                }
            }
    
            public int Count {
                get {
                    return data.Count;
                }
            }
    
            public void Clear() {
                data.Clear();
                index.Clear();
            }
    
            public T First {
                get {
                    return data.First.Value;
                }
            }
        }
    }
    

    LRUCache

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace LRUCache {
        public class LRUCache<TKey, TValue> : IDictionary<TKey, TValue> {
    
            object sync = new object();
            Dictionary<TKey, TValue> data;
            IndexedLinkedList<TKey> lruList = new IndexedLinkedList<TKey>();
            ICollection<KeyValuePair<TKey, TValue>> dataAsCollection;
            int capacity;
    
            public LRUCache(int capacity) {
    
                if (capacity <= 0) {
                    throw new ArgumentException("capacity should always be bigger than 0");
                }
    
                data = new Dictionary<TKey, TValue>(capacity);
                dataAsCollection = data;
                this.capacity = capacity;
            }
    
            public void Add(TKey key, TValue value) {
                if (!ContainsKey(key)) {
                    this[key] = value;
                } else {
                    throw new ArgumentException("An attempt was made to insert a duplicate key in the cache.");
                }
            }
    
            public bool ContainsKey(TKey key) {
                return data.ContainsKey(key);
            }
    
            public ICollection<TKey> Keys {
                get {
                    return data.Keys;
                }
            }
    
            public bool Remove(TKey key) {
                bool existed = data.Remove(key);
                lruList.Remove(key);
                return existed;
            }
    
            public bool TryGetValue(TKey key, out TValue value) {
                return data.TryGetValue(key, out value);
            }
    
            public ICollection<TValue> Values {
                get { return data.Values; }
            }
    
            public TValue this[TKey key] {
                get {
                    var value = data[key];
                    lruList.Remove(key);
                    lruList.Add(key);
                    return value;
                }
                set {
                    data[key] = value;
                    lruList.Remove(key);
                    lruList.Add(key);
    
                    if (data.Count > capacity) {
                        data.Remove(lruList.First);
                        lruList.RemoveFirst();
                    }
                }
            }
    
            public void Add(KeyValuePair<TKey, TValue> item) {
                Add(item.Key, item.Value);
            }
    
            public void Clear() {
                data.Clear();
                lruList.Clear();
            }
    
            public bool Contains(KeyValuePair<TKey, TValue> item) {
                return dataAsCollection.Contains(item);
            }
    
            public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) {
                dataAsCollection.CopyTo(array, arrayIndex);
            }
    
            public int Count {
                get { return data.Count; }
            }
    
            public bool IsReadOnly {
                get { return false; }
            }
    
            public bool Remove(KeyValuePair<TKey, TValue> item) {
    
                bool removed = dataAsCollection.Remove(item);
                if (removed) {
                    lruList.Remove(item.Key);
                }
                return removed;
            }
    
    
            public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() {
                return dataAsCollection.GetEnumerator();
            }
    
    
            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
                return ((System.Collections.IEnumerable)data).GetEnumerator();
            }
    
        }
    }
    

    【讨论】:

    • 我修复了链接,但我记得代码中有一个错误,我会尝试修复它。
    • 我更新了代码,写了一些测试,没有发现任何缺陷,并不是说它没有错误
    • 我认为最好将“最近最少使用”项目的删除放在 add 和 remove 方法中,而不是放在索引器中。我不希望每次访问缓存中的某些内容时都会受到性能影响,因为当我只是访问时,我不在乎缓存是否“已满”。但是,当我添加时,我绝对关心缓存是否已满,并且确实想清除当时最近最少使用的项目。
    • 在 LRUCache 上的 Set 索引器中,我认为您要删除两次: if (data.Count > capacity) { Remove(lruList.First); lruList.RemoveFirst();调用 remove 会删除第一个元素,然后你执行 lruList.RemoveFirst() ... 对吗?
    • @Damian 那是因为它处理缓存过期 afaik
    【解决方案2】:

    对于 .NET 4.0,您还可以使用来自 System.Runtime.CachingMemoryCache

    http://msdn.microsoft.com/en-us/library/system.runtime.caching.aspx

    【讨论】:

      【解决方案3】:

      patterns & practices Enterprise Library(更具体地说,Caching Application Block),但它在 IMO 中往往设计过度且过于复杂。

      【讨论】:

        【解决方案4】:

        .NET Framework 始终能够将weak references 保留给对象。

        基本上,弱引用是对运行时认为“不重要”的对象的引用,并且可能会在任何时间点运行的垃圾回收中删除。例如,这可用于缓存内容,但您无法控制收集哪些内容以及不收集哪些内容。

        另一方面,它使用起来非常简单,可能正是您所需要的。

        戴夫

        【讨论】:

        • 我很久以前就读到过这个,现在忘记了,谢谢!
        • 这类解决方案在这种情况下可能会适得其反。想象一下,您正在预加载图像,然后将它们放入弱引用的哈希表中。如果没有请求进来,并且 GC 运行,它将被删除。然后,当用户滚动时,您需要重新生成它。这一切都可能在几秒钟内发生
        • 预加载和缓存不是一回事,注意不要混淆。
        • 你是对的,但最初的问题对我来说更像是一个预加载场景:“所以,现在,我正在缓存图像用于下一页当用户滚动浏览文档时。”为什么要在滚动之前收集它?为什么之后还要把它们留在记忆中?
        • 在这种情况下,缓存应用程序块也不会有太大用处。应该有人想出一个预加载应用程序块。 ;)
        【解决方案5】:

        典型的权衡情况。将所有内容保存在内存中会很快,但会大大增加内存消耗,而从磁盘中检索会减少内存消耗,但性能不高。不过,这一切你都已经知道了!

        内置的System.Web.Caching.Cache 类很棒,我自己在我的 ASP.NET 应用程序中多次使用它,效果很好(虽然主要用于数据库记录缓存),但是缺点是缓存会只能在一台机器上运行(通常是单独的 Web 服务器),不能分布在多台机器上。

        如果可以“抛出一些硬件”来解决问题,而且它不一定需要昂贵的硬件,只要有足够内存的盒子,你总是可以使用分布式缓存解决方案。这将为您提供更多的内存来玩,同时保持(几乎)相同的性能水平。

        .NET 的分布式缓存解决方案的一些选项是:

        Memcached.NET

        indeXus.Net

        甚至是微软自己的Velocity 项目。

        【讨论】:

        • 所以不是将其缓存在磁盘上,而是通过网络进行?是的,这样会快很多。
        • Dave Van den Eynde:在本地千兆网络上缓存是一种非常常见的方法。然而,在这种情况下这是不可行的。
        【解决方案6】:

        你是如何实现你的缓存的?

        即使在非 Web 应用程序中,您也可以使用 Cache class from System.Web.Caching,它会在需要内存时基于 LRU 清除项目。

        在非 Web 应用程序中,您需要使用 HttpRuntime.Cache 来访问 Cache 实例。

        请注意,文档指出Cache 类不打算在 ASP.NET 之外使用,尽管它总是对我有用。 (不过,我从未在任何关键任务应用程序中依赖它。)

        【讨论】:

        • 哦,哇,我只是为了 ASP.NET 而已。会试试这个:) 现在它只是一个简单的哈希表。
        • Asp.Net 缓存不打算在 Asp.Net 之外使用,因此请务必正确测试您的应用程序。但它当然应该运作良好:-)
        【解决方案7】:

        缓存应用程序块和 ASP.NET 缓存都是选项,尽管它们使用 LRU,但发生的唯一一种磁盘利用率是通过内存分页。我认为有一些方法可以优化它,这些方法更具体到你的目标,以获得更好的输出。以下是一些想法:

        • 既然它是一个 ASP.NET 应用程序,为什么不生成图像,将它们写入磁盘,当浏览器请求下一页时让 IIS 提供它。这使您的 ASP.NET 工作进程保持较低水平,同时让 IIS 做它擅长的事情。
        • 使用有关用户交互方式的统计信息。在 ASP.NET 缓存中实现的 LRU 通常适用于单个图像文件 - 从它的声音来看,这种情况并没有多大用处。取而代之的是一些逻辑,例如:“如果用户在最后 Y 秒内滚动了 X 页,则一般是下一个 X*Y 图像”。用户快速滚动会生成更多页面;缓慢阅读的用户将需要更少的缓存。
        • 让 IIS 从磁盘提供图像,并为您真正想要控制缓存的图像使用自定义 HTTP 处理程序。
        • 让浏览器也提前请求文件,并依赖浏览器缓存。
        • 一旦图像被提供给客户端,可以说它几乎可以从缓存中删除吗?这可以大大减少足迹。

        不过,我当然会避免使用普通的哈希表。

        【讨论】:

          【解决方案8】:

          【讨论】:

            【解决方案9】:

            有一个高效的开源 RAM 虚拟器,它使用 MRU 算法将最新的引用对象保留在内存中,并使用快速、轻量级的后备存储(在磁盘上)进行“分页”。

            这里是代码项目中关于它的小文章的链接:http://www.codeproject.com/Tips/827339/Virtual-Cache

            希望对你有用。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2015-09-03
              • 2012-06-13
              • 2012-06-06
              • 2012-10-14
              • 2018-05-27
              • 2012-08-04
              • 1970-01-01
              相关资源
              最近更新 更多