Подтвердить что ты не робот

Блокировка против ToArray для потокового безопасного доступа foreach к коллекции List

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

Ответ 2

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

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

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

Ответ 3

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

Пример из msdn

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

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

Ответ 4

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

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

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

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>

Ответ 5

Используйте 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 могут вызвать тупик, так как Thread 1 может удерживать A, в то время как Thread 2 удерживает B, и ни один из них не может продолжаться до тех пор, пока другой не отключит его.

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