ConcurrentDictionary.GetOrAdd - 仅在不为null时添加

时间:2015-07-25 16:40:20

标签: c# concurrentdictionary

我使用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;
}

但是,如果用户不为空,我如何检查用户是否从db获取并将用户存储到字典?

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

3 个答案:

答案 0 :(得分:3)

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 User GetUser(int userId)
{
    var user = _user.GetOrAddIfNotNull(userId, GetUserFromDb)   
}

<强>更新

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

  1. 线程1执行GetOrAdd,将null添加到字典并暂停。
  2. 将用户添加到数据库中。
  3. 线程2执行GetOrAdd并从字典中检索null,而不是点击数据库。
  4. 线程1和线程2执行TryRemove并从字典中删除记录。
  5. 通过此时间,线程2将获得null,而不是访问数据库并获取用户记录。如果此边缘情况对您很重要并且您仍想使用ConcurrentDictionary,则可以在扩展方法中使用lock

    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;
            }
        }
    }
    

答案 1 :(得分:2)

这是一个hacky解决方案,我希望有更好的方法。如果找不到用户,则抛出GetUserFromDb。这会将商店中止到字典中。让GetUser捕获异常。这是使用控制流的异常,这是不好的。

答案 2 :(得分:1)

我正在扩展@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;
    }
}