代码之家  ›  专栏  ›  技术社区  ›  EMP

从多个具有相同键/值的线程更新字典是否安全

  •  2
  • EMP  · 技术社区  · 14 年前

    我有静电 System.Collections.Generic.Dictionary<K, V> 用作缓存。在缓存未命中时,我会做一些昂贵的工作来获取指定密钥的值并将其保存在缓存中:

    private static Dictionary<K, V> _cache = ...
    
    public static V GetValue<K>(K key)
    {
        V value;
        if (!_cache.TryGetValue(key, out value))
        {
             value = GetExpensiveValue(key);
             _cache[key] = value; // Thread-safety issue?
        }
    
        return value;
    }
    

    这是一种常见的模式。问题是:当多个线程试图调用 GetValue() 同时呢?

    假设 GetExpensiveValue 总是为一个给定的键返回相同的值,我不在乎哪个线程赢得了竞争条件。是的,这意味着昂贵的操作可能比所需的时间多做一次,但如果读操作比写操作频繁得多,并且这允许我消除锁定,那么这可能是一个折衷办法。但是字典里真的会出什么问题吗?也就是说,在某些情况下,它会抛出异常还是损坏数据?我用一个简单的控制台应用程序测试了一下,没有发现任何问题,但当然这并不意味着它会一直工作。

    2 回复  |  直到 14 年前
        1
  •  1
  •   Marc Gravell    14 年前

    在我看来,这并不安全;字典的内部可能已经在重新编写过程中了。在一个 最低限度 它可能需要读写器锁:

    private readonly static ReaderWriterLockSlim @lock = new ReaderWriterLockSlim();
    public static V GetValue(K key)
    {
        V value;
        @lock.EnterReadLock();
        bool hasValue;
        try
        {
            hasValue = _cache.TryGetValue(key, out value);
        }
        finally
        {
            @lock.ExitReadLock();
        }
    
        if (!hasValue)
        {
            value = GetExpensiveValue(key);
            @lock.EnterWriteLock();
            try
            {
                _cache[key] = value;
            }
            finally
            {
                @lock.ExitWriteLock();
            }
        }
        return value;
    }
    

    如果你只想 GetExpensiveValue(key) 执行一次,移动它 里面 写入锁定并添加双重检查:

            @lock.EnterWriteLock();
            try
            {
                if(!_cache.TryGetValue(key, out value)) { // double-check
                    value = GetExpensiveValue(key);
                    _cache[key] = value;
                }
            }
            finally
            {
                @lock.ExitWriteLock();
            }
    
        2
  •  0
  •   OJ.    14 年前

    如果你不在乎比赛条件,那你应该没事的。如果你关心昂贵的手术 lock 之后 TryGetvalue() 然后打电话 TryGetValue() 再一次

    private static Dictionary<K, V> _cache = ...
    private static object _lockObject = new object();
    
    public static V GetValue<K>(K key)
    {
        V value;
        if (!_cache.TryGetValue(key, out value))
        {
            lock(_lockObject)
            {
                if (!_cache.TryGetValue(key, out value))
                {
                     value = GetExpensiveValue(key);
                     _cache[key] = value; // Thread-safety issue?
                }
            }
        }
    
        return value;
    }
    

    编辑: 考虑到马克的评论,我认为以上不是最好的选择。或许可以考虑使用 ConcurrentDictionary 相反。