Безопасно ли обновлять словарь из нескольких потоков с одинаковым ключом / значением? - PullRequest
2 голосов
/ 24 ноября 2010

У меня есть статический 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 ]

1 голос
/ 24 ноября 2010

Это не выглядит поточно-ориентированным для меня;внутренняя часть словаря уже может быть переписана.При минимуме ему, вероятно, потребуется блокировка чтения / записи:

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();
        }
0 голосов
/ 24 ноября 2010

Если вас не волнуют условия гонки, тогда все будет в порядке. Если вы заботитесь о дорогостоящей операции, просто 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;
}

Edit: После рассмотрения комментария Марка, я думаю, что вышеупомянутое не лучший вариант. Возможно, стоит вместо этого использовать ConcurrentDictionary .

...