3

MVC Web アプリケーションのキャッシング マネージャーに取り組んでいます。このアプリには、構築にコストがかかる非常に大きなオブジェクトがいくつかあります。アプリケーションの有効期間中に、ユーザーの要求に基づいて、これらのオブジェクトをいくつか作成する必要がある場合があります。ビルドすると、ユーザーはオブジェクト内のデータを操作するため、多くの読み取りアクションが発生します。場合によっては、キャッシュされたオブジェクトのいくつかのマイナー データ ポイントを更新する必要があります (作成と置換には時間がかかりすぎます)。

以下は、これを支援するために作成したキャッシュ マネージャー クラスです。基本的なスレッド セーフを超えて、私の目標は次のとおりでした。

  1. オブジェクトに対する複数の読み取りを許可しますが、更新要求時にそのオブジェクトへのすべての読み取りをロックします
  2. オブジェクトがまだ存在しない場合は、オブジェクトが一度だけ作成されるようにします (長いビルド アクションであることに注意してください)。
  3. キャッシュが多くのオブジェクトを格納できるようにし、(すべてのオブジェクトに対して 1 つのロックではなく) オブジェクトごとにロックを維持します。

    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;
        }
    }
    

スレッドセーフを維持しながら目標を達成できていますか? 私の目標を達成するためのより良い方法はありますか?

ありがとう!

更新: つまり、ここでの結論は、1 つの領域で本当に多くのことをしようとしていたということです。何らかの理由で、キャッシュを管理するのと同じクラスで Get / Update 操作を管理するのは良い考えだと確信しました。Groo のソリューションを見て問題を再考した後、かなりの量のリファクタリングを行うことができ、直面していたこの問題を取り除くことができました。

4

1 に答える 1

1

まあ、このクラスはあなたが必要としているものではないと思います。

オブジェクトに対する複数の読み取りを許可しますが、更新要求時にすべての読み取りをロックします

キャッシュ マネージャーへのすべての読み取りをロックすることはできますが、実際のキャッシュされたインスタンスへの読み取り (または更新) をロックしていません。

オブジェクトがまだ存在しない場合は、オブジェクトが一度だけ作成されるようにします (長いビルド アクションであることに注意してください)。

私はあなたがそれを保証したとは思わない。オブジェクトをディクショナリに追加している間は何もロックしていません (さらに、遅延コンストラクターを追加しているため、オブジェクトがいつインスタンス化されるかさえわかりません)。

編集:この部分は保持されます。私が変更する唯一のことは、Getreturn a Lazy<object>. プログラムを書いているときに、キャストするのを忘れてToString、戻り値を呼び出すと「値が作成されていません」が返されました。

キャッシュが多くのオブジェクトを格納できるようにし、(すべてのオブジェクトに対して 1 つのロックではなく) オブジェクトごとにロックを維持します。

これはポイント 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
于 2012-11-08T20:56:48.843 に答える