2008-10-01 7 views
105

я был в состоянии осуществить поточно-словарь в 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(...); 
} 

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

+2

Что это вы найдете некрасиво об этом? – 2008-10-01 14:43:29

+1

Я думаю, что он ссылается на все заявления о блокировке, которые он имеет на протяжении своего кода среди потребителей класса SharedDictionary, - он блокирует код * вызывающего * каждый раз, когда он обращается к методу объекта SharedDictionary. – 2008-10-01 14:54:02

+0

Вместо того, чтобы использовать метод Add, попробуйте сделать, присвоив значения ex-m_MySharedDictionary ["key1"] = "item1", это потокобезопасно. – testuser 2012-04-12 16:53:21

ответ

43

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

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

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

2

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

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

Что происходит в вашем случае заключается в следующем:

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

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

+1

Не верно ... если вы выполняете две операции за другой, которые являются внутренне потокобезопасными, это не означает, что общий блок кода является потокобезопасным. Например: if (! MyDict.ContainsKey (someKey)) { myDict.Add (someKey, someValue); } не был бы потокобезопасным, даже он ContainsKey и Add являются потокобезопасными – Tim 2011-09-02 18:35:57

+0

Ваша точка зрения верна, но не соответствует контексту с моим ответом и вопросом. Если вы посмотрите на вопрос, он не говорит о вызове ContainsKey и не отвечает на мои вопросы. Мой ответ относится к приобретению блокировки SyncRoot, которая показана в примере в исходном вопросе. В контексте оператора блокировки одна или несколько поточно-безопасных операций действительно выполнялись бы безопасно. – 2011-09-06 11:19:18

+0

Я предполагаю, что если все, что он когда-либо делает, это добавить в словарь, но поскольку у него есть «// больше членов IDictionary ...», я предполагаю, что в какой-то момент он также захочет прочитать данные из словаря. Если это так, то должен быть какой-то внешний механизм блокировки. Не имеет значения, является ли это SyncRoot в самом словаре или другом объекте, используемом исключительно для блокировки, но без такой схемы общий код не будет потокобезопасным. – Tim 2011-09-06 18:25:37

6

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

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

4

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

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

Лично я нашел лучший способ реализовать поточно-безопасный класс через неизменность. Это действительно уменьшает количество проблем, с которыми вы можете столкнуться в безопасности потоков. Отъезд Eric Lippert's Blog для более подробной информации.

58

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

0

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

пример

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