Блокировка против ToArray для потокового доступа foreach коллекции List - PullRequest
22 голосов
/ 28 июня 2010

У меня есть коллекция List, и я хочу перебрать ее в многопоточном приложении.Мне нужно защищать его каждый раз, когда я повторяю его, поскольку он может быть изменен, и я не хочу, чтобы исключения «коллекция была изменена» при выполнении foreach.

Как правильно это сделать?*

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

  2. Используйте List <>. ToArray () для копирования в массив каждый раз, когда я выполняю foreach.Это приводит к снижению производительности, но это легко сделать.Я беспокоюсь о том, как трепетает память, а также о времени, чтобы ее скопировать.Просто кажется чрезмерным.Является ли потокобезопасным использование ToArray?

  3. Не используйте foreach, а вместо этого используйте для циклов.Разве мне не нужно проверять длину каждый раз, когда я делаю это, чтобы убедиться, что список не сократился?Это кажется раздражающим.

Ответы [ 5 ]

43 голосов
/ 28 июня 2010

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

  1. Использование блокировки - это нормально, просто убедитесь, что вы используете точно такой же объект блокировки в любом коде, который касается этого списка.Как и код, который добавляет или удаляет элементы из этого списка.Если этот код выполняется в том же потоке, который выполняет итерацию списка, блокировка не требуется.Как правило, единственный шанс для тупика здесь, если у вас есть код, который опирается на состояние потока, например Thread.Join (), в то время как он также содержит этот блокирующий объект.Что должно быть редко.

  2. Да, повторение копии списка всегда поточно-ориентировано, если вы используете блокировку вокруг метода ToArray ().Обратите внимание, что вам все еще нужен замок, без структурного улучшения.Преимущество состоит в том, что вы будете удерживать блокировку на короткое время, улучшая параллелизм в вашей программе.Недостатками являются его требования к O (n)-хранилищу, наличие только безопасного списка, но не защита элементов в списке, и сложная проблема, связанная с постоянным просмотром содержимого списка.Особенно последняя проблема является тонкой и трудно анализируемой.Если вы не можете выяснить побочные эффекты, то вам, вероятно, не следует это учитывать.

  3. Обязательно рассматривайте способность foreach определять расу как дар, а не проблему.,Да, явный цикл for (;;) не будет генерировать исключение, он просто будет работать неправильно.Например, повторять один и тот же элемент дважды или полностью пропустить элемент.Вы можете избежать повторной проверки количества элементов, повторяя их в обратном порядке.Пока другие потоки вызывают только Add (), а не Remove (), который будет вести себя так же, как ToArray (), вы получите устаревшее представление.Не то чтобы это работало на практике, индексирование списка также не поточно-ориентировано.List <> перераспределит свой внутренний массив при необходимости.Это просто не будет работать и работать неправильно непредсказуемым образом.

Здесь есть две точки зрения.Вы можете быть напуганы и следовать здравому смыслу, вы получите программу, которая работает, но не может быть оптимальной.Это мудро и держит босса счастливым.Или вы можете поэкспериментировать и выяснить для себя, как искажение правил приводит к неприятностямЧто сделает вас счастливым, вы будете гораздо лучше программистом.Но ваша производительность будет страдать.Я не знаю, как выглядит твое расписание.

11 голосов
/ 28 июня 2010

Если ваши данные List в основном доступны только для чтения, вы можете разрешить нескольким потокам безопасный доступ к ним одновременно, используя ReaderWriterLockSlim

Вы можете найти реализацию Thread-Safe Dictionary здесь, чтобы начать работу.

Я также хотел упомянуть, что если вы используете .Net 4.0, класс BlockingCollection реализует эту функцию автоматически. Хотел бы я знать об этом несколько месяцев назад!

5 голосов
/ 29 июня 2010

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

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

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

public class ImmutableWidgetList : IEnumerable<Widget>
{
    private List<Widget> _widgets;  // we never modify the list

    // creates an empty list
    public ImmutableWidgetList()
    {
        _widgets = new List<Widget>();
    }

    // creates a list from an enumerator
    public ImmutableWidgetList(IEnumerable<Widget> widgetList)
    {
        _widgets = new List<Widget>(widgetList);
    }

    // add a single item
    public ImmutableWidgetList Add(Widget widget)
    {
        List<Widget> newList = new List<Widget>(_widgets);

        ImmutableWidgetList result = new ImmutableWidgetList();
        result._widgets = newList;
        return result;
    }

    // add a range of items.
    public ImmutableWidgetList AddRange(IEnumerable<Widget> widgets)
    {
        List<Widget> newList = new List<Widget>(_widgets);
        newList.AddRange(widgets);

        ImmutableWidgetList result = new ImmutableWidgetList();
        result._widgets = newList;
        return result;
    }

    // implement IEnumerable<Widget>
    IEnumerator<Widget> IEnumerable<Widget>.GetEnumerator()
    {
        return _widgets.GetEnumerator();
    }


    // implement IEnumerable
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return _widgets.GetEnumerator();
    }
}
  • Я включил IEnumerable<T>, чтобы обеспечить реализацию foreach.
  • Вы упомянули, что беспокоитесь о пространственно-временных показателях создания новых списков, поэтому, возможно, это не сработает для вас.
  • вы также можете захотеть внедрить IList<T>
4 голосов
/ 28 июня 2010

Как правило, коллекции не являются поточно-ориентированными по соображениям производительности, кроме Hash Table.Вы должны использовать IsSynchronized и SyncRoot, чтобы сделать их потокобезопасными.См. здесь и здесь

Пример из msdn

ICollection myCollection = someCollection;
lock(myCollection.SyncRoot)
{
    foreach (object item in myCollection)
    {
        // Insert your code here.
    }
}

Редактировать: если вы используете .net 4.0, вы можете использовать одновременные коллекции

3 голосов
/ 28 июня 2010

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

Тема 1:

lock(A) {
  // .. stuff
  // Next lock request can potentially deadlock with 2
  lock(B) {
    // ... more stuff
  }
}

Тема 2:

lock(B) {
  // Different stuff
  // next lock request can potentially deadlock with 1
  lock(A) {
    // More crap
  }
}

Здесь поток 1 и поток 2 могут вызвать взаимоблокировку, поскольку поток 1 может удерживать A, в то время как поток 2 удерживает B, и ни один из них не может продолжаться, пока другой не снимет свою блокировку.

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

...