如何使用Lazy处理并发请求?

时间:2015-12-13 17:16:57

标签: c#

我是C#的新手,并试图了解如何使用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或我的代码的正确方法是不安全的?

更新

开始使用MemoryCache代替ConcurrentDictionary是不是一个好主意?如果是,则如何创建键值,因为它是string内的MemoryCache.Default.AddOrGetExisting()

2 个答案:

答案 0 :(得分:5)

这是对的。这是一个标准模式(删除除外),它是一个非常好的缓存,因为它可以防止缓存加盖。

我不确定你是否想在计算完成后从缓存中删除,因为计算将一遍又一遍地重做。如果您不需要删除,则可以通过基本删除后半部分来简化代码。

注意,Lazy在异常情况下存在问题:存储异常并且永远不会重新执行工厂。问题永远存在(直到人重新启动应用程序)。在我看来,这使得Lazy在大多数情况下完全不适合生产使用。

这意味着诸如网络问题之类的暂时性错误可能导致应用永久无法使用。

答案 1 :(得分:1)

此答案针对原始问题的更新部分。有关Lazy<T>的线程安全性以及潜在的陷阱,请参阅@usr answer

  

我想知道如何避免使用ConcurrentDictionary<TKey, TValue>并开始   使用MemoryCache?如何实施   MemoryCache.Default.AddOrGetExisting()

如果您正在寻找具有自动过期机制的缓存,那么MemoryCache是一个不错的选择,如果您不想自己实施这些机制。

为了利用强制键的字符串表示的MemoryCache,您需要创建凭证的唯一字符串表示形式,可能是给定的用户ID还是唯一的用户名?

如果可以,您可以创建代表您的唯一标识符的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只保证工厂方法的一次唯一调用。