【问题标题】:Caching Objects with Expensive Build & Allowing Updates使用昂贵的构建和允许更新来缓存对象
【发布时间】:2012-11-08 20:21:05
【问题描述】:

我正在为 MVC Web 应用程序开发缓存管理器。对于这个应用程序,我有一些构建成本很高的非常大的对象。在应用程序生命周期中,我可能需要根据用户请求创建其中几个对象。构建后,用户将使用对象中的数据,从而导致许多读取操作。有时,我需要更新缓存对象中的一些次要数据点(创建和替换会花费太多时间)。

下面是我创建的一个缓存管理器类来帮助我。除了基本的线程安全之外,我的目标是:

  1. 允许对一个对象进行多次读取,但在一次读取时锁定对该对象的所有读取 更新请求
  2. 确保对象只被创建 1 次,如果 它尚不存在(请记住,它的构建时间很长 行动)。
  3. 允许缓存存储许多对象,并保持一个锁 每个对象(而不是所有对象一个锁)。

    public class CacheManager 
    {
        private static readonly ObjectCache Cache = MemoryCache.Default;
        private static readonly ConcurrentDictionary<string, ReaderWriterLockSlim>
             Locks = new ConcurrentDictionary<string, ReaderWriterLockSlim>();
        private const int CacheLengthInHours = 1;
    
        public object AddOrGetExisting(string key, Func<object> factoryMethod)
        {
            Locks.GetOrAdd(key, new ReaderWriterLockSlim());
    
            var policy = new CacheItemPolicy 
              { 
                 AbsoluteExpiration = DateTimeOffset.Now.AddHours(CacheLengthInHours)
              };
            return Cache.AddOrGetExisting
                (key, new Lazy<object>(factoryMethod), policy);
        }
    
        public object Get(string key)
        {
            var targetLock = AcquireLockObject(key);
            if (targetLock != null)
            {
                targetLock.EnterReadLock();
    
                try
                {
                    var cacheItem = Cache.GetCacheItem(key);
                    if(cacheItem!= null)
                        return cacheItem.Value;
                }
                finally 
                {
                    targetLock.ExitReadLock();
                }
            }
    
            return null;
        }
    
        public void Update<T>(string key, Func<T, object> updateMethod)
        {
            var targetLock = AcquireLockObject(key);
            var targetItem = (Lazy<object>) Get(key);
    
            if (targetLock == null || key == null) return;
            targetLock.EnterWriteLock();
    
            try
            {
                updateMethod((T)targetItem.Value);
            }
            finally
            {
                targetLock.ExitWriteLock();
            }
        }
    
        private ReaderWriterLockSlim AcquireLockObject(string key)
        {
            return Locks.ContainsKey(key) ? Locks[key] : null;
        }
    }
    

我是否在保持线程安全的同时实现了我的目标?大家有没有更好的方法来实现我的目标?

谢谢!

更新:所以这里的底线是我真的试图在一个领域做太多事情。出于某种原因,我确信在管理缓存的同一类中管理 Get / Update 操作是个好主意。在查看了 Groo 的解决方案并重新思考了这个问题之后,我能够进行大量的重构,从而消除了我面临的这个问题。

【问题讨论】:

  • 您同时使用CacheConcurrentDictionary?你为什么需要字典?而且您只是锁定对字典的访问,而不是对对象本身的访问。另外,我没有看到您在第一次实例化对象时正在锁定。
  • 我认为您可能需要再次查看代码库,因为字典在此锁定策略中起着至关重要的作用。它保持对与每个缓存项目同步的锁的引用,因此允许我单独锁定每个项目而不会影响其他项目。

标签: c# asp.net-mvc caching


【解决方案1】:

嗯,我认为这门课不能满足你的需要。

允许对对象进行多次读取,但在更新请求时锁定所有读取

您可以将所有读取锁定到 缓存管理器,但您不会将读取(或更新)锁定到实际缓存实例。

确保对象不存在时只创建一次(请记住,这是一个漫长的构建操作)。

我认为你没有确保这一点。在将对象添加到字典时,您没有锁定任何东西(此外,您正在添加一个惰性构造函数,因此您甚至不知道何时实例化该对象)。

编辑: 这部分成立,我唯一要改变的是让Get 返回Lazy&lt;object&gt;。在编写程序时,我忘记转换它并在返回值上调用 ToString `"Value not created"。

允许缓存存储许多对象,并为每个对象维护一个锁(而不是为所有对象一个锁)。

这与第 1 点相同:您正在锁定字典,而不是对对象的访问。你的update 委托有一个奇怪的签名(它接受一个类型化的泛型参数,并返回一个从未使用过的object)。这意味着您实际上是在修改对象的属性,并且这些更改对于您的程序中持有对该对象的引用的任何部分都是立即可见的。

如何解决这个问题

如果您的对象是可变的(我认为它是可变的),则无法确保事务一致性,除非您的每个属性都在每次读取访问时都获得锁定。简化这一点的一种方法是使其不可变(这就是为什么这些在多线程中如此受欢迎的原因)。

或者,您可以考虑将这个大对象分成更小的部分并分别缓存每个部分,如果需要,使它们不可变。

[编辑] 添加了竞态条件示例:

class Program
{
    static void Main(string[] args)
    {
        CacheManager cache = new CacheManager();
        cache.AddOrGetExisting("item", () => new Test());

        // let one thread modify the item
        ThreadPool.QueueUserWorkItem(s =>
        {
            Thread.Sleep(250);
            cache.Update<Test>("item", i =>
            {
                i.First = "CHANGED";
                Thread.Sleep(500);
                i.Second = "CHANGED";

                return i;
            });
        });

        // let one thread just read the item and print it
        ThreadPool.QueueUserWorkItem(s =>
        {
            var item = ((Lazy<object>)cache.Get("item")).Value;
            Log(item.ToString());
            Thread.Sleep(500);
            Log(item.ToString());
        });

        Console.Read();
    }

    class Test
    {
        private string _first = "Initial value";
        public string First
        {
            get { return _first; }
            set { _first = value; Log("First", value); }
        }

        private string _second = "Initial value";
        public string Second
        {
            get { return _second; }
            set { _second = value; Log("Second", value); }
        }

        public override string ToString()
        {
            return string.Format("--> PRINTING: First: [{0}], Second: [{1}]", First, Second);
        }
    }

    private static void Log(string message)
    {
        Console.WriteLine("Thread {0}: {1}", Thread.CurrentThread.ManagedThreadId, message);
    }

    private static void Log(string property, string value)
    {
        Console.WriteLine("Thread {0}: {1} property was changed to [{2}]", Thread.CurrentThread.ManagedThreadId, property, value);
    }
}

应该会发生这样的事情:

t = 0ms  : thread A gets the item and prints the initial value
t = 250ms: thread B modifies the first property
t = 500ms: thread A prints the INCONSISTENT value (only the first prop. changed)
t = 750ms: thread B modifies the second property

【讨论】:

  • 我认为您可能想再查看一次代码。你的断言并不完全正确(我认为)。通过使用 Lazy,我允许框架处理我的线程安全构造。至于锁定读取,我正在使用锁定对象字典与我的缓存配对。我从不锁定对 CacheManager 或字典的访问。字典中的对象被锁定。通过匹配缓存和字典之间的键,我可以处理每个缓存项上的锁。
  • 是的,你说得对,我没有考虑清楚。当两个线程同时调用AddOrGetExisting 时,你会在你的Locks 集合中得到一个唯一的锁对象,而缓存只会得到一个Lazy 实例。主要问题是您正在更新对象的 properties (在 Update 方法内),并且没有任何东西可以确保另一个线程还没有对您的对象的引用。 我从不锁定对 CacheManager 或字典的访问。字典中的对象被锁定了。 - 那么你在Get 方法中做了什么?
  • @Nathan:我们可能彼此不理解,所以我添加了一个示例应用程序来显示我在说什么。我唯一担心的是,锁定在Update 方法中并不能保证对象的状态在程序的其他部分中是一致的。另外,作为一个小细节,我会将Get 方法的签名更改为返回Lazy&lt;object&gt;,因为无论实际类型如何,它总是返回的。另一方面,如果您的实例是不可变的,并且您在更新时交换了字典中的实例,那么我认为它是完全线程安全的。
  • @Nathan:所以,底线是:锁定Update 并使用委托来确保每个人都获得最新实例的想法是好的,如果两个线程竞争更新一个对象,但是锁定Get 方法不会给您带来任何好处。您可能会在具有引用的任何其他线程中获得不一致的读取(当然,没有什么能阻止您更改 Update 方法之外的对象,但我们假设您小心避免这种情况)。
  • 将此答案标记为解决方案,因为它确实是指导我重构底层类的原因。谢谢!
猜你喜欢
  • 2012-06-13
  • 1970-01-01
  • 1970-01-01
  • 2011-03-21
  • 1970-01-01
  • 1970-01-01
  • 2017-06-10
  • 1970-01-01
  • 2016-09-16
相关资源
最近更新 更多