Какой лучший способ реализации потокобезопасного словаря?

Я смог реализовать потокобезопасный словарь в С#, взяв из IDictionary и определяя частный объект SyncRoot:

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public object SyncRoot
    {
        get { return syncRoot; }
    } 

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
    }

    // more IDictionary members...
}

Затем я блокирую этот объект SyncRoot на всех моих потребителях (несколько потоков):

Пример:

lock (m_MySharedDictionary.SyncRoot)
{
    m_MySharedDictionary.Add(...);
}

Мне удалось заставить его работать, но это привело к некоторому уродливому коду. Мой вопрос в том, есть ли лучший, более элегантный способ реализации потокобезопасного словаря?

Ответ 1

Как сказал Питер, вы можете инкапсулировать всю безопасность потока внутри класса. Вам нужно быть осторожным с любыми событиями, которые вы раскрываете или добавляете, чтобы убедиться, что они вызваны вне любых блокировок.

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
        OnItemAdded(EventArgs.Empty);
    }

    public event EventHandler ItemAdded;

    protected virtual void OnItemAdded(EventArgs e)
    {
        EventHandler handler = ItemAdded;
        if (handler != null)
            handler(this, e);
    }

    // more IDictionary members...
}

Изменить: Документы MSDN указывают, что перечисление по сути не является потокобезопасным. Это может быть одной из причин раскрытия объекта синхронизации вне вашего класса. Еще один способ приблизиться к этому - предоставить некоторые методы для выполнения действий для всех членов и заблокировать перечисление членов. Проблема заключается в том, что вы не знаете, вызывает ли действие, переданное этой функции, какой-либо член вашего словаря (что приведет к тупиковой ситуации). Предоставление объекта синхронизации позволяет потребителю принимать эти решения и не скрывать тупик внутри вашего класса.

Ответ 2

Класс .NET 4.0, который поддерживает concurrency, называется ConcurrentDictionary.

Ответ 3

Попытка синхронизировать внутренне почти наверняка будет недостаточной, поскольку она при слишком низком уровне абстракции. Предположим, что вы выполняете операции Add и ContainsKey индивидуально в потоковом режиме следующим образом:

public void Add(TKey key, TValue value)
{
    lock (this.syncRoot)
    {
        this.innerDictionary.Add(key, value);
    }
}

public bool ContainsKey(TKey key)
{
    lock (this.syncRoot)
    {
        return this.innerDictionary.ContainsKey(key);
    }
}

Тогда что происходит, когда вы вызываете этот предположительно потокобезопасный бит кода из нескольких потоков? Будет ли он работать нормально?

if (!mySafeDictionary.ContainsKey(someKey))
{
    mySafeDictionary.Add(someKey, someValue);
}

Простой ответ - нет. В какой-то момент метод Add выдаст исключение, указывающее, что ключ уже существует в словаре. Как это может быть с нитевидным словарем, вы можете спросить? Хорошо, потому что каждая операция является потокобезопасной, комбинация двух операций не является, поскольку другой поток может изменить ее между вашим вызовом до ContainsKey и Add.

Что означает правильно писать этот тип сценария, вам нужна блокировка вне словаря, например.

lock (mySafeDictionary)
{
    if (!mySafeDictionary.ContainsKey(someKey))
    {
        mySafeDictionary.Add(someKey, someValue);
    }
}

Но теперь, видя, что вам приходится писать код внешней блокировки, вы смешиваете внутреннюю и внешнюю синхронизацию, что всегда приводит к таким проблемам, как нечеткий код и взаимоблокировки. Поэтому в конечном итоге вам, вероятно, и лучше:

  • Использовать нормальный Dictionary<TKey, TValue> и синхронизировать извне, включая составные операции над ним или

  • Напишите новую потокобезопасную оболочку с другим интерфейсом (т.е. не IDictionary<T>), который объединяет такие операции, как метод AddIfNotContained, поэтому вам никогда не нужно комбинировать операции с ним.

    /li >

(Я обычно ухожу С# 1)

Ответ 4

Вы не должны публиковать свой закрытый объект блокировки через свойство. Объект блокировки должен существовать конфиденциально с единственной целью действовать как точка рандеву.

Если производительность оказывается недостаточной с использованием стандартного замка, то может быть очень полезно использовать Wintellect Power Threading сборник блокировок.

Ответ 5

Существует несколько проблем с описанным методом реализации.

  1. Вы никогда не должны раскрывать свой объект синхронизации. Это позволит открыть себе потребителя, захватившего объект, и заблокировать его, а затем вы будете тосты.
  2. Вы внедряете безопасный интерфейс, не связанный с потоком, с потокобезопасным классом. ИМХО это будет стоить вам по дороге

Лично я нашел лучший способ реализовать класс, защищенный потоками, через неизменность. Это действительно уменьшает количество проблем, с которыми вы можете столкнуться в безопасности потоков. Подробнее см. Блог Eric Lippert.

Ответ 6

Вам не нужно блокировать свойство SyncRoot в ваших потребительских объектах. Блока, который у вас есть в методах словаря, достаточно.

Чтобы разработать: Что в конечном итоге происходит, так это то, что ваш словарь заблокирован в течение более длительного периода времени, чем это необходимо.

Что происходит в вашем случае:

Скажите, что поток A получает блокировку на SyncRoot перед вызовом m_mySharedDictionary.Add. Затем поток B пытается получить блокировку, но заблокирован. Фактически, все остальные потоки заблокированы. В Thread A разрешен вызов метода Add. В инструкции lock в методе Add поток A разрешен для повторной блокировки, поскольку он уже владеет им. После выхода из контекста блокировки внутри метода, а затем вне метода, поток A освободил все блокировки, позволяющие продолжить работу других потоков.

Вы можете просто разрешить любому потребителю вызывать метод Add в качестве оператора блокировки в классе SharedDictionary. Метод Add будет иметь тот же эффект. На данный момент у вас есть резервная блокировка. Вы бы заблокировали SyncRoot вне одного из методов словаря, если вам пришлось выполнить две операции над объектом словаря, которые должны быть гарантированы, чтобы они выполнялись последовательно.

Ответ 7

Просто мысль, почему бы не воссоздать словарь? Если чтение представляет собой множество записей, тогда блокировка будет синхронизировать все запросы.

Пример

    private static readonly object Lock = new object();
    private static Dictionary<string, string> _dict = new Dictionary<string, string>();

    private string Fetch(string key)
    {
        lock (Lock)
        {
            string returnValue;
            if (_dict.TryGetValue(key, out returnValue))
                return returnValue;

            returnValue = "find the new value";
            _dict = new Dictionary<string, string>(_dict) { { key, returnValue } };

            return returnValue;
        }
    }

    public string GetValue(key)
    {
        string returnValue;

        return _dict.TryGetValue(key, out returnValue)? returnValue : Fetch(key);
    }