Как использовать 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.