如何使用Lazy处理并发请求

本文关键字:并发 请求 处理 Lazy 何使用 | 更新日期: 2023-09-27 18:26:46

我是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是个好主意吗?如果是,如何创建键值,因为它是MemoryCache.Default.AddOrGetExisting() 中的string

如何使用Lazy处理并发请求

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

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

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

这意味着,网络问题等暂时性错误可能会使应用程序永久不可用。

此答案指向原始问题的更新部分。请参阅有关Lazy<T>线程安全性和潜在陷阱的@usr答案。


我想知道如何避免使用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仅保证对工厂方法的单个唯一调用。