IEnumerable <T> безопасность потоков?

У меня есть основной поток, который заполняет List<T>. Далее я создаю цепочку объектов, которые будут выполняться в разных потоках, требуя доступа к списку. Первоначальный список никогда не будет записан после его создания. Моя мысль заключалась в том, чтобы передать список как IEnumerable<T> для объектов, выполняющихся на других потоках, главным образом из-за того, что они не позволяют тем, кто реализует эти объекты, записывать в список по ошибке. Другими словами, если исходный список гарантированно не записывается, безопасно ли для нескольких потоков использовать .Where или foreach в IEnumerable?

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

Ответ 1

IEnumerable<T> не может быть изменен. Итак, что может быть с ним небезопасным? (Если вы не изменяете фактический List<T>).

Для обеспечения безопасности без потоков вам понадобятся операции записи и чтения.

"Итератор сам по себе" создается для каждого foreach.

Изменить: Я немного упростил свой ответ, но @Eric Lippert добавил ценный комментарий. IEnumerable<T> не определяет методы модификации, но это не означает, что операторы доступа являются потокобезопасными (GetEnumerator, MoveNext и т.д.). Простейший пример: GetEnumerator реализован как это:

  • Каждый раз возвращает тот же экземпляр IEnumerator
  • Сбрасывает позицию

Более сложным примером является кэширование.

Это интересный момент, но, к счастью, я не знаю какого-либо стандартного класса, который не имеет потокобезопасной реализации IEnumerable.

Ответ 2

Каждый поток, который вызывает Where или foreach, получает свой собственный счетчик - они не делят один объект перечислителя для того же списка. Так как список не изменяется, и поскольку каждый поток работает со своей собственной копией перечислителя, не должно быть проблем с безопасностью потока.

Вы можете видеть, что это работает в одном потоке. Просто создайте список из 10 объектов и получите два перечисления из этого списка. Используйте один перечислитель для перечисления по 5 элементам, а другой - для перечисления по 5 элементам. Вы увидите, что оба перечисления перечислили только первые 5 элементов, а второй не запустился, когда первый счетчик остановился.

Ответ 3

Пока вы уверены, что List никогда не будет изменен, тогда будет безопасно читать из нескольких потоков. Это включает использование экземпляров IEnumerator, которые он предоставляет.

Это будет справедливо для большинства коллекций. Фактически, все коллекции в BCL должны быть стабильными во время перечисления. Другими словами, перечислитель не будет изменять структуру данных. Я могу подумать о некоторых неясных случаях, например, о splay-tree, перечислять, что он может изменить структуру. Опять же, ни одна из коллекций BCL не делает этого.

Ответ 4

Если вы уверены, что список не будет изменен после создания, вы должны гарантировать, что путем преобразования его в ReadOnlyCollection <T> . Конечно, если вы сохраните исходный список, который используется только для чтения, вы можете его изменить, но если вы выбросите исходный список, вы эффективно сделаете его permentantly только для чтения.

В разделе "Безопасность потока" коллекции:

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

Итак, если вы не коснетесь первоначального списка еще раз и прекратите ссылаться на него, вы можете убедиться, что несколько потоков могут прочитать его без беспокойства (пока вы не делаете ничего пугающего с попыткой изменить его снова).

Ответ 5

Другими словами, если исходный список гарантированно не записывается, безопасно ли использовать несколько потоков .Where или foreach на IEnumerable?

Да, это только проблема, если список мутирован.

Но обратите внимание, что IEnumerable<T> можно отбросить в список и затем изменить.

Но есть и другая альтернатива: заверните свой список в ReadOnlyCollection<T> и передайте его. Если вы сейчас выбросите исходный список, вы в основном создали новый неизменный список.