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

发布于 2024-10-03 23:08:52 字数 697 浏览 0 评论 0原文

我有一个静态 System.Collections.Generic.Dictionary 用作缓存。在缓存未命中时,我会执行一些昂贵的工作来获取指定键的值并将其保存在缓存中:

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() 时,这是否安全或者可能会失败?

假设 GetExpectiveValue 始终为给定键返回相同的值,我并不真正关心哪个线程赢得竞争条件。是的,这意味着昂贵的操作可能会比需要的次数多执行一次,但如果读取比写入频繁得多,那么我可能会对此感到满意,并且这允许我消除锁定。但词典内部真的会出问题吗?也就是说,在某些情况下它会抛出异常或损坏其数据吗?我用一个简单的控制台应用程序对此进行了测试,没有发现任何问题,但这当然并不意味着它总是有效。

I have a static System.Collections.Generic.Dictionary<K, V> that's used as a cache. On a cache miss I do some expensive work to get the value for the specified key and save it in the cache:

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

This is a common pattern. The question is: is this safe or could it fail when multiple threads try to call GetValue() at the same time?

Assuming that GetExpensiveValue always returns the same value for a given key, I don't really care which thread wins the race condition. Yes, it means that the expensive operation is potentially done one more time than needed, but that's a trade-off I may be happy with if reads are far more frequent than writes and this allows me to eliminate locking. But would something actually go wrong inside the Dictionary? That is, would it throw an exception or corrupt its data in some cases? I tested this out with a simple console app and found no issues, but of course that doesn't mean it will always work.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

拥醉 2024-10-10 23:08:52

对我来说这看起来不是线程安全的;字典的内部可能已经在被重写的过程中。 至少它可能需要一个读/写锁:

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

如果您只想 GetExpectiveValue(key) 执行一次,请将其移入写锁并添加双重检查:

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

That doesn't look thread-safe to me; the inside of the dictionary could already be in the process of being re-written. At a minimum it probably needs a reader/writer lock:

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

and if you only want GetExpensiveValue(key) to execute once, move it inside the write-lock and add a double-check:

        @lock.EnterWriteLock();
        try
        {
            if(!_cache.TryGetValue(key, out value)) { // double-check
                value = GetExpensiveValue(key);
                _cache[key] = value;
            }
        }
        finally
        {
            @lock.ExitWriteLock();
        }
吲‖鸣 2024-10-10 23:08:52

如果你不关心竞争条件那么你应该没问题。如果您关心昂贵的操作,只需在 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;
}

编辑:
考虑到Marc的评论后,我认为上述并不是最好的选择。也许考虑使用 ConcurrentDictionary 代替。

If you don't care about race conditions then you should be fine. If you care about the expensive operation, just lock after TryGetvalue() and then call TryGetValue() again

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

Edit:
After consideration of Marc's comment, I think the above is not the best option. Perhaps consider using the ConcurrentDictionary instead.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文