Async threadsafe Получить из MemoryCache
Я создал асинхронный кеш, который использует .NET MemoryCache
под ним.
Это код
public async Task<T> GetAsync(string key, Func<Task<T>> populator, TimeSpan expire, object parameters)
{
if(parameters != null)
key += JsonConvert.SerializeObject(parameters);
if(!_cache.Contains(key))
{
var data = await populator();
lock(_cache)
{
if(!_cache.Contains(key)) //Check again but locked this time
_cache.Add(key, data, DateTimeOffset.Now.Add(expire));
}
}
return (T)_cache.Get(key);
}
Я думаю, что единственным недостатком является то, что мне нужно ждать ожидания за пределами блокировки, поэтому populator не является потокобезопасным, но, поскольку он не может находиться внутри замка, я думаю, что это лучший способ. Есть ли какие-то подводные камни, которые я пропустил?
Обновление: версия ответа Esers, которая также является потоковой, когда поток antoher делает недействительным кеш
public async Task<T> GetAsync(string key, Func<Task<T>> populator, TimeSpan expire, object parameters)
{
if(parameters != null)
key += JsonConvert.SerializeObject(parameters);
var lazy = new Lazy<Task<T>>(populator, true);
_cache.AddOrGetExisting(key, lazy, DateTimeOffset.Now.Add(expire));
return ((Lazy<Task<T>>) _cache.Get(key)).Value;
}
Однако он может быть медленнее, поскольку создает Lazy-экземпляры, которые никогда не будут выполняться, и использует Lazy в полном потоковом режиме LazyThreadSafetyMode.ExecutionAndPublication
Обновление с новым эталоном (более высокое)
Lazy with lock 42535929
Lazy with GetOrAdd 41070320 (Only solution that is completely thread safe)
Semaphore 64573360
Ответы
Ответ 1
Простое решение - использовать SemaphoreSlim.WaitAsync()
вместо блокировки, а затем вы можете обойти проблему ожидания внутри блокировки. Хотя все другие методы MemoryCache
являются потокобезопасными.
private SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1);
public async Task<T> GetAsync(
string key, Func<Task<T>> populator, TimeSpan expire, object parameters)
{
if (parameters != null)
key += JsonConvert.SerializeObject(parameters);
if (!_cache.Contains(key))
{
await semaphoreSlim.WaitAsync();
try
{
if (!_cache.Contains(key))
{
var data = await populator();
_cache.Add(key, data, DateTimeOffset.Now.Add(expire));
}
}
finally
{
semaphoreSlim.Release();
}
}
return (T)_cache.Get(key);
}
Ответ 2
Хотя есть уже принятый ответ, я отправлю новый с подходом Lazy<T>
. Идея заключается в следующем: чтобы минимизировать продолжительность блока lock
, если ключ не существует в кеше, поместите кеш Lazy<T>
в кеш. Таким образом, все потоки, использующие один и тот же ключ, будут ждать одно и то же значение Lazy<T>
public Task<T> GetAsync<T>(string key, Func<Task<T>> populator, TimeSpan expire, object parameters)
{
if (parameters != null)
key += JsonConvert.SerializeObject(parameters);
lock (_cache)
{
if (!_cache.Contains(key))
{
var lazy = new Lazy<Task<T>>(populator, true);
_cache.Add(key, lazy, DateTimeOffset.Now.Add(expire));
}
}
return ((Lazy<Task<T>>)_cache.Get(key)).Value;
}
Version2
public Task<T> GetAsync<T>(string key, Func<Task<T>> populator, TimeSpan expire, object parameters)
{
if (parameters != null)
key += JsonConvert.SerializeObject(parameters);
var lazy = ((Lazy<Task<T>>)_cache.Get(key));
if (lazy != null) return lazy.Value;
lock (_cache)
{
if (!_cache.Contains(key))
{
lazy = new Lazy<Task<T>>(populator, true);
_cache.Add(key, lazy, DateTimeOffset.Now.Add(expire));
return lazy.Value;
}
return ((Lazy<Task<T>>)_cache.Get(key)).Value;
}
}
Version3
public Task<T> GetAsync<T>(string key, Func<Task<T>> populator, TimeSpan expire, object parameters)
{
if (parameters != null)
key += JsonConvert.SerializeObject(parameters);
var task = (Task<T>)_cache.Get(key);
if (task != null) return task;
var value = populator();
return
(Task<T>)_cache.AddOrGetExisting(key, value, DateTimeOffset.Now.Add(expire)) ?? value;
}
Ответ 3
Текущие ответы используют несколько устаревшие System.Runtime.Caching.MemoryCache
. Они также содержат тонкие условия гонки (см. комментарии). Наконец, не все из них позволяют тайм-ауту зависеть от кэшируемого значения.
Вот моя попытка использовать новый Microsoft.Extensions.Caching.Memory (используется ASP.NET Core):
//Add NuGet package: Microsoft.Extensions.Caching.Memory
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;
MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
public Task<T> GetOrAddAsync<T>(
string key, Func<Task<T>> factory, Func<T, TimeSpan> expirationCalculator)
{
return _cache.GetOrCreateAsync(key, async cacheEntry =>
{
var cts = new CancellationTokenSource();
cacheEntry.AddExpirationToken(new CancellationChangeToken(cts.Token));
var value = await factory().ConfigureAwait(false);
cts.CancelAfter(expirationCalculator(value));
return value;
});
}
Пример использования:
await GetOrAddAsync("foo", () => Task.Run(() => 42), i => TimeSpan.FromMilliseconds(i)));
Обратите внимание, что фабричный метод не может быть вызван только один раз (см. https://github.com/aspnet/Caching/issues/240).
Ответ 4
Это попытка улучшения ответа Eser answer (версия 2). Класс Lazy
по умолчанию является потокобезопасным, поэтому lock
можно удалить. Возможно, для данного ключа будет создано несколько объектов Lazy
, но только у одного из них будет запрошено свойство Value
, что вызовет запуск тяжелого Task
. Другой Lazy
останется неиспользованным, выйдет из области видимости и вскоре станет сборщиком мусора.
Первая перегрузка является гибкой и общей и принимает аргумент Func<CacheItemPolicy>
. Я включил еще две перегрузки для наиболее распространенных случаев абсолютного и скользящего истечения. Для удобства можно добавить еще много перегрузок.
using System.Runtime.Caching;
static partial class MemoryCacheExtensions
{
public static Task<T> GetOrCreateLazyAsync<T>(this MemoryCache cache, string key,
Func<Task<T>> valueFactory, Func<CacheItemPolicy> cacheItemPolicyFactory)
{
var lazyTask = (Lazy<Task<T>>)cache.Get(key);
if (lazyTask != null) return lazyTask.Value.ToAsyncConditional();
lazyTask = new Lazy<Task<T>>(valueFactory);
var cacheItem = new CacheItem(key, lazyTask);
var cacheItemPolicy = cacheItemPolicyFactory?.Invoke();
var existingCacheItem = cache.AddOrGetExisting(cacheItem, cacheItemPolicy);
return ((Lazy<Task<T>>)(existingCacheItem?.Value ?? cacheItem.Value)).Value
.ToAsyncConditional();
}
public static Task<T> GetOrCreateLazyAsync<T>(this MemoryCache cache, string key,
Func<Task<T>> valueFactory, DateTimeOffset absoluteExpiration)
{
return cache.GetOrCreateLazyAsync(key, valueFactory, () => new CacheItemPolicy()
{
AbsoluteExpiration = absoluteExpiration,
});
}
public static Task<T> GetOrCreateLazyAsync<T>(this MemoryCache cache, string key,
Func<Task<T>> valueFactory, TimeSpan slidingExpiration)
{
return cache.GetOrCreateLazyAsync(key, valueFactory, () => new CacheItemPolicy()
{
SlidingExpiration = slidingExpiration,
});
}
private static Task<TResult> ToAsyncConditional<TResult>(this Task<TResult> task)
{
if (task.IsCompleted) return task;
return task.ContinueWith(async t => await t,
default, TaskContinuationOptions.RunContinuationsAsynchronously,
TaskScheduler.Default).Unwrap();
}
}
Пример использования:
string html = await MemoryCache.Default.GetOrCreateLazyAsync("MyKey", async () =>
{
return await new WebClient().DownloadStringTaskAsync("https://stackoverflow.com");
}, DateTimeOffset.Now.AddMinutes(10));
HTML-код этого сайта загружается и кэшируется в течение 10 минут. Несколько одновременных запросов await
будут выполнять одну и ту же задачу.
Класс System.Runtime.Caching.MemoryCache
прост в использовании, но имеет ограниченную поддержку для определения приоритетов записей кэша. В основном есть только две опции, Default
и NotRemovable
, что означает, что они вряд ли подходят для сложных сценариев. Более новый класс Microsoft.Extensions.Caching.Memory.MemoryCache
(из этого пакета) предлагает дополнительные параметры в отношении приоритетов кэша (Low
, Normal
, High
и NeverRemove
]), но в остальном менее интуитивно понятен и более громоздок в использовании. Он предлагает асинхронные возможности, но не ленивый. Итак, вот эквивалентные расширения LazyAsync для этого класса:
using Microsoft.Extensions.Caching.Memory;
static partial class MemoryCacheExtensions
{
public static Task<T> GetOrCreateLazyAsync<T>(this MemoryCache cache, object key,
Func<ICacheEntry, Task<T>> factory)
{
return cache.GetOrCreate(key, e =>
{
return new Lazy<Task<T>>(() => factory(e));
}).Value.ToAsyncConditional();
}
public static Task<T> GetOrCreateLazyAsync<T>(this MemoryCache cache, object key,
Func<Task<T>> valueFactory, DateTimeOffset absoluteExpiration)
{
return cache.GetOrCreateLazyAsync(key, e =>
{
e.AbsoluteExpiration = absoluteExpiration;
return valueFactory();
});
}
public static Task<T> GetOrCreateLazyAsync<T>(this MemoryCache cache, object key,
Func<Task<T>> valueFactory, TimeSpan slidingExpiration)
{
return cache.GetOrCreateLazyAsync(key, e =>
{
e.SlidingExpiration = slidingExpiration;
return valueFactory();
});
}
}
Пример использования:
var cache = new MemoryCache(new MemoryCacheOptions());
string html = await cache.GetOrCreateLazyAsync("MyKey", async () =>
{
return await new WebClient().DownloadStringTaskAsync("https://stackoverflow.com");
}, DateTimeOffset.Now.AddMinutes(10));
Обновление: я только что узнал о специфической функции механизма async
- await
. Когда неполное Task
ожидается несколько раз одновременно, продолжения будут выполняться синхронно (в одном и том же потоке) одно за другим (при условии отсутствия контекста синхронизации). Это может быть проблемой для вышеупомянутых реализаций GetOrCreateLazyAsync
, потому что блокировка кода может существовать сразу после ожидаемого вызова GetOrCreateLazyAsync
, и в этом случае будут затронуты другие ожидающие (задержанные или даже заблокированные). Возможное решение этой проблемы - вернуть асинхронное продолжение лениво созданного Task
вместо самой задачи, но только если задача не завершена. Это является причиной для введения метода ToAsyncConditional
выше.