C# ConcurrentDictionary.GetOrAdd-仅当不为null时添加

C# ConcurrentDictionary.GetOrAdd-仅当不为null时添加,c#,concurrentdictionary,C#,Concurrentdictionary,我使用ConcurrentDictionary以并行访问的方式缓存数据,有时新项目可以存储在db中,而不会加载到缓存中。这就是我使用GetOrAdd的原因 public User GetUser(int userId) { return _user.GetOrAdd(userId, GetUserFromDb); } private User GetUserFromDb(int userId) { var user = _unitOfWork.UserRepos

我使用ConcurrentDictionary以并行访问的方式缓存数据,有时新项目可以存储在db中,而不会加载到缓存中。这就是我使用GetOrAdd的原因

public User GetUser(int userId)
{
    return _user.GetOrAdd(userId, GetUserFromDb);        
}

private User GetUserFromDb(int userId)
{
    var user = _unitOfWork.UserRepository.GetById(userId);

    // if user is null, it is stored to dictionary

    return user;
}
但我如何检查用户是否是从数据库中获取的,并且仅当用户不为null时才将用户存储到字典中呢


可能我可以在GetOrAdd之后立即从ConcurrentDictionary中删除null,但它看起来不太线程安全,而且不是非常优雅的解决方案。从字典中插入和删除无用的内容。你知道怎么做吗?

这里有一个简单的解决方案,我希望有更好的办法。如果找不到用户,则抛出
GetUserFromDb
throw。这将中止存储到字典中。使
GetUser
捕获异常。这是对控制流使用异常,这不好

public User GetUser(int userId)
{
    var user = _user.GetOrAdd(userId, GetUserFromDb);
    if (user == null) _user.TryRemove(userId, out user);    
}
您还可以将其包装到扩展方法中:

public static TValue GetOrAddIfNotNull<TKey, TValue>(
    this ConcurrentDictionary<TKey, TValue> dictionary,
    TKey key, 
    Func<TKey, TValue> valueFactory) where TValue : class
{
    var value = dictionary.GetOrAdd(key, valueFactory);
    if (value == null) dictionary.TryRemove(key, out value);
    return value;
}
public static class ConcurrentDictionaryExtensions
{
    private static readonly object myLock = new object();

    public static TValue GetOrAddIfNotNull<TKey, TValue>(
        this ConcurrentDictionary<TKey, TValue> dictionary,
        TKey key, 
        Func<TKey, TValue> valueFactory) where TValue : class
    {
        lock (myLock)
        {
            var value = dictionary.GetOrAdd(key, valueFactory);
            if (value == null) dictionary.TryRemove(key, out value);
            return value;
        }
    }
}
更新

根据@usr评论,可能存在以下情况:

  • 线程1执行
    GetOrAdd
    ,将
    null
    添加到字典中并暂停
  • 将用户添加到数据库中
  • 线程2执行
    GetOrAdd
    并从字典中检索
    null
    ,而不是访问数据库
  • 线程1和线程2执行
    TryRemove
    并从字典中删除记录
  • 通过此计时,线程2将获得
    null
    ,而不是命中数据库并获取用户记录。如果此边缘大小写对您很重要,并且您仍然希望使用
    ConcurrentDictionary
    ,那么您可以在扩展方法中使用
    lock

    public static TValue GetOrAddIfNotNull<TKey, TValue>(
        this ConcurrentDictionary<TKey, TValue> dictionary,
        TKey key, 
        Func<TKey, TValue> valueFactory) where TValue : class
    {
        var value = dictionary.GetOrAdd(key, valueFactory);
        if (value == null) dictionary.TryRemove(key, out value);
        return value;
    }
    
    public static class ConcurrentDictionaryExtensions
    {
        private static readonly object myLock = new object();
    
        public static TValue GetOrAddIfNotNull<TKey, TValue>(
            this ConcurrentDictionary<TKey, TValue> dictionary,
            TKey key, 
            Func<TKey, TValue> valueFactory) where TValue : class
        {
            lock (myLock)
            {
                var value = dictionary.GetOrAdd(key, valueFactory);
                if (value == null) dictionary.TryRemove(key, out value);
                return value;
            }
        }
    }
    
    公共静态类ConcurrentDictionaryExtensions
    {
    私有静态只读对象myLock=new object();
    公共静态TValue GetOrAddifyNotNull(
    这本词典,
    TKey键,
    Func valueFactory),其中TValue:class
    {
    锁(myLock)
    {
    var value=dictionary.GetOrAdd(key,valueFactory);
    if(value==null)dictionary.TryRemove(key,out值);
    返回值;
    }
    }
    }
    
    我正在扩展@NikolaiSamteladze解决方案,以包括双重检查锁定,以便其他线程可以在字典更新后跳过获取锁定

    public static class ConcurrentDictionaryExtensions
    {
        private static readonly object myLock = new object();
    
        public static TValue GetOrAddIfNotNull<TKey, TValue>(
            this ConcurrentDictionary<TKey, TValue> dictionary,
            TKey key,
            Func<TKey, TValue> valueFactory) where TValue : class
        {
            TValue value;
            if (!dictionary.TryGetValue(key, out value))
            {
                lock (myLock)
                {
                    value = dictionary.GetOrAdd(key, valueFactory);
                    if (value == null) dictionary.TryRemove(key, out value);
                } 
            }
            return value;
        }
    }
    
    公共静态类ConcurrentDictionaryExtensions
    {
    私有静态只读对象myLock=new object();
    公共静态TValue GetOrAddifyNotNull(
    这本词典,
    TKey键,
    Func valueFactory),其中TValue:class
    {
    t价值;
    if(!dictionary.TryGetValue(键,输出值))
    {
    锁(myLock)
    {
    value=dictionary.GetOrAdd(key,valueFactory);
    if(value==null)dictionary.TryRemove(key,out值);
    } 
    }
    返回值;
    }
    }
    
    如果
    GetOrAdd
    返回null,则可以
    TryRemove
    删除添加的值。但这不是一个很好的解决方案。如果数据库中的查找返回null,用户是否可以稍后出现在数据库中,这是您想要处理的吗?如果没有,那么用户ID存储在字典中又有什么关系呢?如果找不到,下次请求该用户ID时,字典已缓存该用户ID不存在,因此不需要询问数据库。@y0io您最终使用了什么?@Nikolai Samteladze I稍后将返回此问题,但使用锁定扩展方法的解决方案可能会获胜;)非常感谢谢谢,但是它是线程安全的吗?假设在GetOrAdd之后,其他进程将新用户添加到字典中,然后TryRemove删除现有用户。这种情况发生的可能性很小,但也可能发生。从某种意义上讲,这是一种快速性,即两个线程可能运行GetOrAddifyNotNull,当用户不存在时,只有一个线程可能会访问数据库。听起来像是良性的种族@y0io是的,也是这样。@y0io不确定另一个进程如何将用户添加到字典中,因为在执行
    GetOrAdd
    之后,已经有一个用户具有该
    userId
    。你能解释一下吗@usr我同意第二个线程可能不会命中数据库。但这假设用户是在第一个线程上的
    GetOrAdd
    TryRemove
    之间添加到数据库的。问题是您是否需要这种精度,以及不从数据库检索此用户的连续性是什么。它不再是具有此全局锁的并发字典。OK。ConcurrentDictionary允许对独立键执行并行操作。现在,有一个全局锁。一次只能有一个线程访问字典。这是安全的,但不可扩展。不再使用CD了。字典现在在这种模式下会更合适。是的,它应该可以工作,但我希望它一定有更好的解决方案。我已经给出了+1,但我自己的(有限)测试表明,它不会带来很大的速度提升。@辩手如果你大部分时间都在获取值而不是添加值,那么它确实会带来很大的不同,因为如果TryGetValue返回值,它将跳过锁获取。此设计模式专门用于此目的。