Как использовать Lazy для обработки параллельного запроса?
Я новичок в С# и пытаюсь понять, как работать с Lazy
.
Мне нужно обработать параллельный запрос, ожидая результат уже запущенной операции. Запросы на данные могут поступать одновременно с одинаковыми/разными учетными данными.
Для каждого уникального набора учетных данных может быть не более одного входящего вызова GetDataInternal, при этом результат этого одного вызова возвращается всем официантам в очереди, когда он готов
private readonly ConcurrentDictionary<Credential, Lazy<Data>> Cache
= new ConcurrentDictionary<Credential, Lazy<Data>>();
public Data GetData(Credential credential)
{
// This instance will be thrown away if a cached
// value with our "credential" key already exists.
Lazy<Data> newLazy = new Lazy<Data>(
() => GetDataInternal(credential),
LazyThreadSafetyMode.ExecutionAndPublication
);
Lazy<Data> lazy = Cache.GetOrAdd(credential, newLazy);
bool added = ReferenceEquals(newLazy, lazy); // If true, we won the race.
Data data;
try
{
// Wait for the GetDataInternal call to complete.
data = lazy.Value;
}
finally
{
// Only the thread which created the cache value
// is allowed to remove it, to prevent races.
if (added) {
Cache.TryRemove(credential, out lazy);
}
}
return data;
}
Правильно ли это использовать Lazy
или мой код небезопасен?
Update:
Можно ли начать использовать MemoryCache
вместо ConcurrentDictionary
? Если да, то как создать значение ключа, потому что оно string
внутри MemoryCache.Default.AddOrGetExisting()
Ответы
Ответ 1
Это правильно. Это стандартный шаблон (за исключением удаления), и это действительно хороший кеш, поскольку он предотвращает печать кеша.
Я не уверен, что вы хотите удалить из кеша, когда вычисление будет выполнено, потому что вычисление будет переделано снова и снова. Если вам не требуется удаление, вы можете упростить код, в основном, удалив вторую половину.
Обратите внимание, что Lazy
имеет проблему в случае исключения: исключение сохраняется и factory никогда не будет повторно выполняться. Проблема сохраняется навсегда (пока пользователь не перезапустит приложение). По моему мнению, это делает Lazy
совершенно непригодным для использования в большинстве случаев.
Это означает, что временная ошибка, такая как проблема с сетью, может сделать приложение недоступным навсегда.
Ответ 2
Этот ответ направлен на обновленную часть исходного вопроса. См. @usr в отношении безопасности потоков с помощью Lazy<T>
и потенциальных ловушек.
Я хотел бы знать, как избежать использования ConcurrentDictionary<TKey, TValue>
и начать используя MemoryCache? Как реализовать MemoryCache.Default.AddOrGetExisting()
?
Если вы ищете кэш, у которого есть механизм автоматического истечения срока действия, то MemoryCache
- хороший выбор, если вы не хотите сами внедрять механику.
Чтобы использовать MemoryCache
, который заставляет строковое представление для ключа, вам нужно создать уникальное строковое представление учетных данных, возможно, заданного идентификатора пользователя или уникального имени пользователя?
Если вы можете, вы можете создать переопределение ToString
, которое представляет ваш уникальный идентификатор, или просто использовать указанное свойство, и использовать MemoryCache
следующим образом:
public class Credential
{
public Credential(int userId)
{
UserId = userId;
}
public int UserId { get; private set; }
}
Теперь ваш метод будет выглядеть следующим образом:
private const EvictionIntervalMinutes = 10;
public Data GetData(Credential credential)
{
Lazy<Data> newLazy = new Lazy<Data>(
() => GetDataInternal(credential), LazyThreadSafetyMode.ExecutionAndPublication);
CacheItemPolicy evictionPolicy = new CacheItemPolicy
{
AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(EvictionIntervalMinutes)
};
var result = MemoryCache.Default.AddOrGetExisting(
new CacheItem(credential.UserId.ToString(), newLazy), evictionPolicy);
return result != null ? ((Lazy<Data>)result.Value).Value : newLazy.Value;
}
MemoryCache
предоставляет вам поточно-безопасную реализацию, это означает, что два потока, обращающихся к AddOrGetExisting
, будут только добавлять или извлекать один элемент кэша. Кроме того, Lazy<T>
с ExecutionAndPublication
гарантирует только единственный уникальный вызов метода factory.