Томский политехнический университет
Опубликован: 23.01.2013 | Доступ: платный | Студентов: 21 / 4 | Длительность: 12:09:00
Лекция 10:

Параллельные коллекции

Аннотация: В рамках данной лекции будут рассмотрены следующие вопросы: классы параллельных коллекций; интерфейс IProducerConsumerCollection<T>; пример использования обычной коллекции с применением параллелизма.

Классы параллельных коллекций

Начиная с версии .NET Framework 1.0 коллекции определены в пространстве имен System.Collections. Эти коллекции, которые содержат класс ArrayList и класс Hashtable, предоставляют некоторую потокобезопасность с помощью свойства Synchronized, которое возвращает потокобезопасную программу-оболочку вокруг коллекции. Работа программы оболочки заключается в блокировке всей коллекции при каждой операции добавления или удаления. Поэтому каждый поток, который пытается получить доступ к коллекции, должен ждать своей очереди для получения блокировки. Такой подход не является масштабируемым и может привести к значительному снижению производительности для больших коллекций.

В версии .NET Framework 2.0 классы коллекций находятся в пространстве имен System.Collections.Generic. Они включают классы List<T>, Dictionary<TKey, TValue> и так далее. Эти классы предоставляют улучшенную безопасность типа и производительность по сравнению с классами на платформе .NET Framework 1.0. Однако классы коллекций платформы .NET Framework 2.0 не обеспечивают синхронизацию потоков. Пользовательский код должен обеспечивать всю синхронизацию при параллельном добавлении элементов в несколько потоков или удалении элементов из них.

В версии .NET Framework 4.0 стало доступно новое пространство имен System.Collections.Concurrent, которое содержит несколько классов коллекций, являющимися потокобезопасными и масштабируемыми. Это означает, что несколько потоков могут безопасно и эффективно добавлять и удалять элементы из таких коллекций, не требуя при этом дополнительной синхронизации в пользовательском коде. Параллельные коллекции отличаются от стандартных коллекций и тем, что они содержат специальные методы для выполнения атомарных операций типа "проверить-и-выполнить" (методы TryPop, TryAdd). В Табл. 14.1 перечислены новые классы параллельных коллекций, которые были добавлены в .NET Framework 4.0

Таблица 14.1. Краткое описание классов параллельных коллекций
Класс Описание
BlockingCollection<T> Предоставляет возможности блокировки и ограничения для потокобезопасных коллекций, реализующих IProducerConsumerCollection<T>. Потоки-производители блокируются, если слоты отсутствуют или коллекция является полной. Потоки-потребители блокируются, если коллекция пуста. Этот тип также поддерживает не блокирующий доступ потребителей и производителей. Коллекцию BlockingCollection<T> можно использовать в качестве базового класса или резервного хранилища для предоставления блокировки и ограничения для любого класса коллекции, поддерживающего IEnumerable<T>.
ConcurrentBag<T> Потокобезопасная реализация наборов, предоставляющая масштабируемые операции добавления и получения.
ConcurrentDictionary<TKey, TValue> Тип параллельного и масштабируемого словаря.
ConcurrentQueue<T> Параллельная и масштабируемая очередь FIFO.
ConcurrentStack<T> Параллельный и масштабируемый стек LIFO.

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

  • Параллельные коллекции следует использовать в тех случаях, когда имеются сценарии с высокой конкурентностью за ресурсы компьютера. В противном случае используются обычные коллекции.
  • Параллельные коллекции не гарантируют потокобезопасность;
  • Если в процессе перебора элементов параллельной коллекции другой поток ее модифицирует, исключение сгенерировано не будет. Вместо этого получается коллекция со старым и новым содержимым;
  • Не существует параллельной версии коллекции List<T>;
  • Параллельные классы стека, очереди и набора (bag) внутри реализованы на основе связных списков. Это делает их менее эффективными в плане потребления памяти по сравнению с непараллельными версиями классов Stack и Queue, но более предпочтительными для параллельного доступа, поскольку связные списки являются отличными кандидатами для lock-free или low-lock реализаций.

Использование параллельных коллекций не эквивалентно использованию обычных коллекций с операторами lock. Например, использование параллельной коллекции ConcurrentDictionary будет выполняться медленнее в данном случае:

var d = new ConcurrentDictionary<int,int>();
for (int i = 0; i < 1000000; i++) d[i] = 123;

Нежели использование обычной коллекции Dictionary
var d = new Dictionary<int,int>();
for (int i = 0; i < 1000000; i++) lock (d) d[i] = 123;

Примечание. Если необходимо считать из коллекции ConcurrentDictionary, то операция выполняется быстрее, поскольку чтение являюется lock-free.

Интерфейс IProducerConsumerCollection<T>

Данный интерфейс обеспечивает унифицированное представление для коллекций производителей/потребителей, чтобы абстракции более высокого уровня, такие как BlockingCollection<T>, могли использовать коллекцию в качестве базового механизма хранения. Интерфейс IProducerConsumerCollection<T> был добавлен в версию .NET 4 для поддержки новых, безопасных в отношении потоков классов коллекций. Существует два основных сценария использования коллекций типа поставщик/потребитель (producer/consumer):

  • Добавление элементов ("поставка");
  • Получение элемента и его одновременное удаление ("потребление").

Синтаксис используемый при создании интерфейса представлен ниже:

public interface IProducerConsumerCollection<T> : IEnumerable<T>, 
ICollection, IEnumerable

где T - Определяет тип элементов коллекции.

Классическим примером использования этого интерфейса являются стеки и очереди. Следующие классы реализуют этот интерфейс:

  • ConcurrentStack<T>;
  • ConcurrentQueue<T>;
  • ConcurrentBag<T>.

Интерфейс IProducerConsumerCollection<T> расширяет интерфейс ICollection<T> путем добавления методов и свойств представленных в Табл. 14.2.

Таблица 14.2. Свойства и методы интерфейса IProducerConsumerCollection<T>
Имя Описание
CopyTo(T[] array, int index); Метод копирует элементы коллекции IProducerConsumerCollection<T> в массив.
ToArray(T item) Метод копирует элементы, содержащиеся в коллекции IProducerConsumerCollection<T>, в новый массив.
TryAdd(T item) Метод пытается добавить объект в коллекцию IProducerConsumerCollection<T>.
TryTake(out T item) Метод пытается удалить и вернуть объект из коллекции IProducerConsumerCollection<T>.
IEnumerable.GetEnumerator() Метод возвращает перечислитель, который выполняет итерацию по элементам коллекции. (Унаследовано от IEnumerable.)
IEnumerator<T> GetEnumerator() Метод возвращает перечислитель, выполняющий перебор элементов в коллекции. (Унаследовано от IEnumerable<T>.)
Count Свойство возвращает число элементов, содержащихся в коллекции ICollection. (Унаследовано от ICollection.)
IsSynchronized Данное свойство получает значение, позволяющее определить, является ли доступ к коллекции ICollection синхронизированным (потокобезопасным).
SyncRoot Свойство получает объект, который можно использовать для синхронизации доступа к ICollection.

Методы TryAdd() и TryTake() проверяют, может ли быть выполнена операция добавления/удаления элемента, и если операция может быть выполнена, то она выполняется.

Метод TryTake() возвращает значение false, если коллекция пуста. Метод TryAdd() всегда завершается успешно и возвращает true во всех трех существующих реализациях. Если вы напишете свою собственную параллельную коллекцию, которая будет запрещать дубликаты, то она сможет возвращать false, если такой элемент уже существует в коллекции.

Конкретный элемент, который удаляется при вызове метода TryTake(), определяется конкретной реализацией:

  • В классе ConcurrentStack<T> - метод TryTake() удаляет последний добавленный элемент;
  • В классе ConcurrentQueue<T> - метод TryTake() удаляет самый первый добавленный элемент;
  • В классе ConcurrentBag<T> - метод TryTake() удаляет любой элемент, который может быть удален, максимально эффективно.

Эти классы реализуют методы TryTake() и TryAdd(), явно предоставляя ту же самую функциональность с помощью других открытых методов с более точными названиями, такими как TryDequeue() и TryPop().

Владимир Каширин
Владимир Каширин

Вопрос по Курсу: "Параллельное программирование с использованием MS VisualStudia 2010".

При компиляции Самостоятельного задания (одновременная отрисовка прямоугольников, эллипсов и выдача в текст-бокс случайного числа) среда предупреждает: suspend - устаревшая команда; примените monitor, mutex и т.п.

Создаётся впечатление, что Задание создано в более поздней среде, чем VS 2010.