長時間実行されるプロセスなどの特定の状況では、ASP.NETキャッシュをロックして、そのリソースに対する別のユーザーによる後続の要求が、キャッシュにアクセスする代わりに長いプロセスを再度実行しないようにすることが重要であることを知っています。
ASP.NETでキャッシュロックを実装するためのC#の最良の方法は何ですか?
回答:
基本的なパターンは次のとおりです。
コードでは、次のようになります。
private static object ThisLock = new object();
public string GetFoo()
{
// try to pull from cache here
lock (ThisLock)
{
// cache was empty before we got the lock, check again inside the lock
// cache is still empty, so retreive the value here
// store the value in the cache here
}
// return the cached value here
}
完全を期すために、完全な例は次のようになります。
private static object ThisLock = new object();
...
object dataObject = Cache["globalData"];
if( dataObject == null )
{
lock( ThisLock )
{
dataObject = Cache["globalData"];
if( dataObject == null )
{
//Get Data from db
dataObject = GlobalObj.GetData();
Cache["globalData"] = dataObject;
}
}
}
return dataObject;
globalObject
実際には存在しないスコープに戻ってきます。何が起こるかというと、それ dataObject
は最終的なnullチェック内で使用されるべきであり、globalObjectはイベントが存在する必要はまったくありません。
キャッシュインスタンス全体をロックする必要はありません。挿入する特定のキーをロックするだけで済みます。つまり、男性用トイレを使用している間、女性用トイレへのアクセスをブロックする必要はありません:)
以下の実装では、並行辞書を使用して特定のキャッシュキーをロックできます。このようにして、2つの異なるキーに対して同時にGetOrAdd()を実行できますが、同じキーに対して同時に実行することはできません。
using System;
using System.Collections.Concurrent;
using System.Web.Caching;
public static class CacheExtensions
{
private static ConcurrentDictionary<string, object> keyLocks = new ConcurrentDictionary<string, object>();
/// <summary>
/// Get or Add the item to the cache using the given key. Lazily executes the value factory only if/when needed
/// </summary>
public static T GetOrAdd<T>(this Cache cache, string key, int durationInSeconds, Func<T> factory)
where T : class
{
// Try and get value from the cache
var value = cache.Get(key);
if (value == null)
{
// If not yet cached, lock the key value and add to cache
lock (keyLocks.GetOrAdd(key, new object()))
{
// Try and get from cache again in case it has been added in the meantime
value = cache.Get(key);
if (value == null && (value = factory()) != null)
{
// TODO: Some of these parameters could be added to method signature later if required
cache.Insert(
key: key,
value: value,
dependencies: null,
absoluteExpiration: DateTime.Now.AddSeconds(durationInSeconds),
slidingExpiration: Cache.NoSlidingExpiration,
priority: CacheItemPriority.Default,
onRemoveCallback: null);
}
// Remove temporary key lock
keyLocks.TryRemove(key, out object locker);
}
}
return value as T;
}
}
keyLocks.TryRemove(key, out locker)
<=それの用途は何ですか?
factory
メソッドを同時に実行しているエッジケースです。T1が以前factory
にnullを返したものを実行した場合のみ(したがって、値はキャッシュされません)。それ以外の場合、T2とT3はキャッシュされた値を同時に取得します(これは安全なはずです)。簡単な解決策は削除することkeyLocks.TryRemove(key, out locker)
だと思いますが、多数の異なるキーを使用すると、ConcurrentDictionaryがメモリリークになる可能性があります。それ以外の場合は、おそらくセマフォを使用して、削除する前にキーのロックをカウントするロジックを追加しますか?
Pavelが言ったことをエコーするだけで、これが最もスレッドセーフな書き方だと思います
private T GetOrAddToCache<T>(string cacheKey, GenericObjectParamsDelegate<T> creator, params object[] creatorArgs) where T : class, new()
{
T returnValue = HttpContext.Current.Cache[cacheKey] as T;
if (returnValue == null)
{
lock (this)
{
returnValue = HttpContext.Current.Cache[cacheKey] as T;
if (returnValue == null)
{
returnValue = creator(creatorArgs);
if (returnValue == null)
{
throw new Exception("Attempt to cache a null reference");
}
HttpContext.Current.Cache.Add(
cacheKey,
returnValue,
null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
System.Web.Caching.Cache.NoSlidingExpiration,
CacheItemPriority.Normal,
null);
}
}
}
return returnValue;
}
Craig Shoemakerは、asp.netキャッシングで優れたショーを行いました:http: //polymorphicpodcast.com/shows/webperformance/
私は次の拡張方法を考え出しました:
private static readonly object _lock = new object();
public static TResult GetOrAdd<TResult>(this Cache cache, string key, Func<TResult> action, int duration = 300) {
TResult result;
var data = cache[key]; // Can't cast using as operator as TResult may be an int or bool
if (data == null) {
lock (_lock) {
data = cache[key];
if (data == null) {
result = action();
if (result == null)
return result;
if (duration > 0)
cache.Insert(key, result, null, DateTime.UtcNow.AddSeconds(duration), TimeSpan.Zero);
} else
result = (TResult)data;
}
} else
result = (TResult)data;
return result;
}
@JohnOwenと@ user378380の両方の回答を使用しました。私のソリューションでは、int値とbool値をキャッシュ内に格納することもできます。
エラーがあったり、もう少し上手く書けるかどうか訂正してください。
最近、Correct State Bag Access Patternと呼ばれる1つのパターンを見ましたが、これはこれに触れているようです。
スレッドセーフになるように少し変更しました。
http://weblogs.asp.net/craigshoemaker/archive/2008/08/28/asp-net-caching-and-performance.aspx
private static object _listLock = new object();
public List List() {
string cacheKey = "customers";
List myList = Cache[cacheKey] as List;
if(myList == null) {
lock (_listLock) {
myList = Cache[cacheKey] as List;
if (myList == null) {
myList = DAL.ListCustomers();
Cache.Insert(cacheKey, mList, null, SiteConfig.CacheDuration, TimeSpan.Zero);
}
}
}
return myList;
}
myList
変数ではなく、キャッシュを再度チェックする必要があります
Insert
例外を防ぐために使用する場合は、ロックがDAL.ListCustomers
1回呼び出されることを確認する場合にのみ、ロックは必要ありません(ただし、結果がnullの場合は、毎回呼び出されます)。
CodeGuruのこの記事では、さまざまなキャッシュロックのシナリオと、ASP.NETキャッシュロックのいくつかのベストプラクティスについて説明しています。
私はその特定の問題を解決するライブラリを書きました:Rocks.Caching
柔軟性を高めるために@ user378380のコードを変更しました。TResultを返す代わりに、さまざまなタイプを順番に受け入れるためのオブジェクトを返すようになりました。また、柔軟性のためにいくつかのパラメーターを追加します。すべてのアイデアは@ user378380に属しています。
private static readonly object _lock = new object();
//If getOnly is true, only get existing cache value, not updating it. If cache value is null then set it first as running action method. So could return old value or action result value.
//If getOnly is false, update the old value with action result. If cache value is null then set it first as running action method. So always return action result value.
//With oldValueReturned boolean we can cast returning object(if it is not null) appropriate type on main code.
public static object GetOrAdd<TResult>(this Cache cache, string key, Func<TResult> action,
DateTime absoluteExpireTime, TimeSpan slidingExpireTime, bool getOnly, out bool oldValueReturned)
{
object result;
var data = cache[key];
if (data == null)
{
lock (_lock)
{
data = cache[key];
if (data == null)
{
oldValueReturned = false;
result = action();
if (result == null)
{
return result;
}
cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
}
else
{
if (getOnly)
{
oldValueReturned = true;
result = data;
}
else
{
oldValueReturned = false;
result = action();
if (result == null)
{
return result;
}
cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
}
}
}
}
else
{
if(getOnly)
{
oldValueReturned = true;
result = data;
}
else
{
oldValueReturned = false;
result = action();
if (result == null)
{
return result;
}
cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
}
}
return result;
}