Словарь .NET: потенциальные проблемы параллелизма? - PullRequest
8 голосов
/ 29 января 2009

Я работаю над обслуживанием проекта .NET, и у меня возникли некоторые проблемы, с которыми я с удовольствием поделюсь с вами, ребята =)

Код проблемы:

if( evilDict.Count < 1 )
{
    foreach (Item item in GetAnotherDict())
        if (!evilDict.containsKey(item.name.ToLower().Trim()))
            evilDict.add(item.name.ToLower().Trim(), item.ID);
}

Несмотря на проверку contains (), я получаю ArgumentException, сообщающий, что элемент с таким же ключом уже добавлен. Мы только столкнулись с этой проблемой на производстве, а не на тестировании, что заставляет меня заподозрить проблему параллелизма. Что мне интересно, так это:

  • Как вы думаете, это проблема параллелизма?
  • Как мне это исправить?
  • Является ли мое исправление жизнеспособным (см. Ниже)?
  • Это мой первый удар в .NET, словари, как правило, являются источником проблем?

Вот мое потенциальное исправление, заменив функцию dictionary.add ()

protected static void DictAddHelper(Dictionary<String, int> dict, String key, int value)
{
    lock (dict)
    {
        key = key.ToLower().Trim();
        if (dict.ContainsKey(key) == false)
        {
            try
            {
                dict.Add(key, value);
            }
            catch (ArgumentException aex)
            {
                StringBuilder debugInfo = new StringBuilder();
                debugInfo.AppendLine("An argumentException has occured: " + aex.Message);
                debugInfo.AppendLine("key = " + key);
                debugInfo.AppendLine("value = " + value);
                debugInfo.AppendLine("---Dictionary contains---");

                foreach (String k in dict.Keys)
                    debugInfo.AppendLine(k + " = " + dict[k]);

                log.Error(debugInfo, aex);
            }
        }
    }
}

EDIT:

Предложения, которые не требуют от меня создания поточно-ориентированной реализации класса Dict, лучше, так как это будет довольно большой рефакторинг, который не будет очень желанным предложением =)

EDIT2:

Я пытался

lock (((IDictionary)dict).SyncRoot)

Но я получаю

Error   28  Using the generic type 'System.Collections.Generic.IDictionary<TKey,TValue>' requires '2' type arguments    

Тогда я попробую это:

lock (((IDictionary<String, int>)dict).SyncRoot)

Ошибка:

Error   28  'System.Collections.Generic.IDictionary<string,int>' does not contain a definition for 'SyncRoot'

ЗАКЛЮЧИТЕЛЬНОЕ РЕДАКТИРОВАНИЕ (наверное):

Спасибо за все ответы!

Теперь все, что я хочу знать, это. Мой метод (DictAddHelper) будет работать вообще, и если нет, то почему?

Ответы [ 11 ]

6 голосов
/ 29 января 2009

Если вы подозреваете, что у вас есть проблемы с параллелизмом при доступе к словарю, ваше исправление не будет иметь никакого смысла. Это решит конкретную проблему, с которой вы столкнулись, однако, если у вас есть параллельный доступ к словарю, у вас в будущем возникнет больше проблем.

Рассмотрите возможность блокировки доступа к словарю при его изменении.

3 голосов
/ 29 января 2009

с использованием примера ng5000, модифицированного по предложенной стратегии блокировки, даст нам:

public static class ThreadSafeDictionary
{
    private static readonly Dictionary<string, int> dict = new Dictionary<string, int>();

    public static void AddItem(string key, int value)
    {
        lock (((IDictionary)dict).SyncRoot)
        {
            if (!dict.ContainsKey(key))
                dict.Add(key, value);
        }
    }
}

наслаждайся. , ,

Jimi

РЕДАКТИРОВАТЬ: обратите внимание, что класс должен быть статическим, чтобы иметь смысл этого примера! ;)

2 голосов
/ 29 января 2009

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

Я рекомендую использовать SynchronizedKeyedCollection, хотя для вас это может быть слишком много для повторного факторинга, поскольку члены для запроса не совпадают с членами словаря.

2 голосов
/ 29 января 2009

Первый код (при условии, что тип Dictionary - System.Collections.Generic.Dictionary) не будет компилироваться. Нет общедоступного содержимого (и метода Contains).

Тем не менее, существует вероятность того, что у вас может возникнуть проблема параллелизма. Как вы и предполагали, другой поток может изменить словарь после проверки ContainsKey и перед вставкой. Чтобы исправить это, оператор блокировки - это путь.

Одно замечание: я бы предпочел, чтобы словарь был обернут в потокобезопасный класс, что-то вроде (предостережения: не завершено, не предназначено в качестве ссылочного кода и может быть улучшено):

public class ThreadSafeDictionary
{
    private Dictionary<string, int> dict = new Dictionary<string, int>();
    private object padlock = new object();

    public void AddItem( string key, int value )
    {
        lock ( padlock )
        {
            if( !dict.ContainsKey( key ) )
                dict.Add( key, value );
        }
    }
}

Как реализовать поточно-ориентированные словари, было полностью описано здесь .

1 голос
/ 29 января 2009

Ace

Создайте новое приложение WindowsFormsApplication (.Net 2.0) и вставьте код в код Form1.cs. (возможно, вам придется изменить имя пространства имен с WindowsFormsApplication1 на любое, по умолчанию используемое вашей системой). Также добавьте командную кнопку в форму. Во всяком случае, здесь все это:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        private readonly Dictionary<string, int> localDict1 = new Dictionary<string, int>();
        private readonly Dictionary<string, string> localDict2 = new Dictionary<string, string>();

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // use two different dictionaries just to show it working with
            // different data types (i.e. we could use class objects too)
            if (localDict1 != null)
            {
                ThreadSafeDictionaryStatic.AddItem(localDict1, "New Item :1", 1);
                ThreadSafeDictionaryStatic.AddItem(localDict1, "New Item :2", 2);
            }
            if (localDict2 != null)
                ThreadSafeDictionaryStatic.AddItem(localDict2, "New Item :1", "this is not a number");
        }
    }

    public static class ThreadSafeDictionaryStatic
    {

        public static void AddItem<T>(IDictionary<string, T> dict, string key, T value)
        {
            if (dict == null) return;
            lock (((IDictionary)dict).SyncRoot)
            {
                if (!dict.ContainsKey(key))
                    dict.Add(key, value);
                else
                {
                    // awful and we'd never ever show a message box in real life - however!!
                    var result = dict[key];
                    MessageBox.Show(String.Format("Key '{0}' is already present with a value of '{1}'", key, result));
                }
            }
        }

        public static T GetItem<T>(IDictionary<string, T> dict, string key)
        {
            if (dict == null) return default(T);
            if (dict.ContainsKey(key))
                return dict[key];
            else
                return default(T);
        }

        public static bool Remove<T>(IDictionary<string, T> dict, string key)
        {
            if (dict == null) return false;
            lock (((IDictionary)dict).SyncRoot)
            {
                if (dict.ContainsKey(key))
                    return dict.Remove(key);
            }
            return false;
        }
    }
}

Дайте мне знать, как это происходит ...

РЕДАКТИРОВАТЬ: заново сделал класс, соответствующий сигнатуре вашего метода, также использовал дженерики, так как вы хотите, чтобы метод принимал ЛЮБОЙ вид объекта. вы можете легко изменить его обратно, удалив <T> ссылки и т.д ..

1 голос
/ 29 января 2009

Ace - странно, что он не работает в вашем коде, он, безусловно, работает в соответствии с примером (я знаю, что это вам не поможет, просто любопытство !!).

Надеюсь, у вас все получится, я уверен, что это будет довольно простой причиной неудачи. Вы можете даже попробовать этот пример вместо своего кода, чтобы увидеть, не является ли это какой-то другой проблемой.

1 голос
/ 29 января 2009

В .NET Framework встроен поточно-безопасный словарный класс, который уже предлагает хорошее начало для решения вашей проблемы, которое действительно может быть связано с параллелизмом.

Это абстрактный класс с именем SynchronizedKeyedCollection (K, T) , из которого можно получить производный метод и добавить метод, содержащий вызов метода Contains, а затем Add внутри блокировки, которая блокирует base.SyncRoot.

1 голос
/ 29 января 2009
    lock (((IDictionary)dict).SyncRoot)
    {
        if(!dict.Contains( .. ))
        dict.Add ( );
    }

это работает для меня (см. Пример обновления ng5000 ниже):)

1 голос
/ 29 января 2009

Трудно сказать, действительно ли это проблема параллелизма. Если к словарю обращаются из нескольких потоков, то это действительно может быть проблемой параллелизма, но я думаю, что вы должны добавить код трассировки, чтобы выяснить, кто является виновником.

Если к словарю обращаются из нескольких потоков, вам действительно следует убедиться, что методы Contains (или ContainsKey) и Add вызываются в одной атомарной транзакции. Чтобы сделать это, вы действительно должны вызывать эти 2 метода в пределах блокировки.

lock( dict.SyncRoot )
{
   if( dict.Contains( .. ) == false )
      dict.Add ( );
}
1 голос
/ 29 января 2009

Не должно

if (!evilDict.contains(item.name.ToLower().Trim()))

быть

if (!evilDict.ContainsKey(item.name.ToLower().Trim()))

...