Каков наилучший способ реализации многопоточного словаря? - PullRequest
106 голосов
/ 01 октября 2008

Мне удалось реализовать потокобезопасный словарь в C #, унаследовав от 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(...);
}

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

Ответы [ 8 ]

204 голосов
/ 13 сентября 2010

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

58 голосов
/ 30 декабря 2008

Попытки внутренней синхронизации почти наверняка будут недостаточными, поскольку уровень абстракции слишком низок. Скажем, вы делаете операции 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);
    }
}

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

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

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

(Я, как правило, сам иду с # 1)

43 голосов
/ 01 октября 2008

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

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 указано, что перечисление по своей природе не является потокобезопасным. Это может быть одной из причин выставления объекта синхронизации за пределы вашего класса. Другой способ приблизиться к этому - предоставить некоторые методы для выполнения действий над всеми участниками и ограничить перечисление членов. Проблема в том, что вы не знаете, вызывает ли действие, переданное этой функции, некоторый член вашего словаря (что приведет к тупику). Предоставление объекта синхронизации позволяет потребителю принимать эти решения и не скрывает тупик внутри вашего класса.

6 голосов
/ 01 октября 2008

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

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

4 голосов
/ 01 октября 2008

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

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

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

2 голосов
/ 01 октября 2008

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

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

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

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

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

0 голосов
/ 16 ноября 2012

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

пример

    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);
    }
0 голосов
/ 01 октября 2008
...