Andriy Shyrokoryadov

.Net developer, data scientist

Многопоточность - коллекции - вопрос №20 на собеседование C# / .NET

Текст к видео "Многопоточность - коллекции" на канале YouTube

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

  1. Поток А проверяет если в словаре элемент с ключом Х и получает отрицательный результат;
  2. Поток Б также проверяет если в словаре элемент с ключом Х и получает отрицательный результат;
  3. Поток А добавляет в словарь элемент с ключом Х – операция успешна;
  4. Поток Б добавляет в словарь элемент с ключом Х – операция завершается ошибкой, элемент с ключом Х уже существует в словаре и этот элемент был добавлен потоком А;

Данная ошибка появилась по 2 причинам:

  • проверка наличия элемента в словаре и добавление элемента не является атомарной операцией
  • отсутствие синхронизации доступа коллекции между потоками – в любой момент времени к коллекции может иметь доступ любой поток.

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

Одной из такой коллекции является коллекция ConcurentBag<T>. Сразу следует сказать, что все коллекции, которые мы будем сегодня рассматривать, имеют в своем название слово Concurent. Как правило такое название коллекции обозначает, что данная коллекция предназначена для работы в многопоточной среде. Возвращаясь к коллекции ConcurentBag – это просто «мешок» для переменных, в который можно вкидывать дубликаты и нет особого порядка элементов. Самые важные методы этой коллекции – это Add, TryTake, TryPeek. Также эта коллекции имплементирует интерфейс IEnumerable<T>. Вы можете перебирать элементы этой коллекции используя петлю foreach, о которой у меня на канале снято видео. В многопоточной среде перебор элементов следующим образом. Когда вы запускаете перебор вы получаете копию коллекции и пока идёт перебор, вы не будете видеть изменения состояния коллекции – то есть вы не увидите ново добавленные элементы, а также вам будут видны элементы, которые были удалены другими потоками во время перебора.

Следующими коллекциями для работы в конкурентной среде являются ConcurrentStack<T> и ConcurrentQueue<T>. Кроме многопоточности здесь следует сказать пару слов как работает коллекция типа стэк и коллекция типа очередь. С очередью все понятно – первый прибыл, первый выбыл. Очереди и их принцип мы можем увидеть в разных местах. Например, как кто-то пошутил, в филиалах ада на земле: в поликлиниках, в собесе и на почте. Что касается стэка, тот здесь всё чуть более интересней. Часто кандидат на вопрос приведите примеры стэка из реальной жизни затрудняется дать ответ. Стэк это коллекция, в которой первый прибыл, последний выбыл или последний прибыл, первый выбыл. Например, стэком являются тарелки в стопке на полке: последняя тарелка, которую поставили в стопку на полку, будет взята первой. Также корзины, сложенные в стопку в супермаркете, являются примером стэка. Или, например чипсы Pringles в цилиндрической упаковке – после открытия упаковки вы возьмете первый чипc, который был положен в упаковку последним. Самое главное, что необходимо запомнить о данных коллекциях это то, что нет возможности доступа к элементам коллекции в произвольном порядке. В любой определенный момент времени доступен только один элемент коллекции. Для стэка – это элемент, который был добавлен последним, а для очереди – это элемент, который был добавлен первым среди всех существующих элементов в коллекции.

Коллекция типа ConcurrentStack<T> имеет 2 важных метода: Push и TryPop. Первый метод используется для добавления элемента в коллекцию, а второй для получения элемента из коллекции. Слово try в названии метода означает, что коллекция может вообще не иметь элементов, так как она может быть изменена несколькими потоками.

Коллекция типа ConcurrentQueue<T> имеет 2 важных метода: Enqueue и TryDequeue. Первый метод используется для добавления элемента в коллекцию, а второй для получения элемента из коллекции. Также есть метод TryPeek, который позволяется получить элемент коллекции, не удаляя его из коллекции в отличие от метода TryDequeue.

Следующим типом коллекции для работы в многопоточной среде, который мы рассмотрим, является ConcurrentDictionary. Данная коллекция содержит в себе пары «ключ»-«значение», например «машина»-«BMW». Все операции связанные с работой коллекции Dictionary, то есть добавление, удаление и актуализации элементов, выполняются как одношаговые атомарные операции. Что очень существенно влияет на удобство работы с этим типом коллекции в многопоточной среде.

Последним типом коллекции, который мы сегодня рассмотрим, будет коллекция типа BlockingCollection<T>. Это простая коллекция для добавления и удаления данных в многопоточной среде. Коллекция называется блокирующей, потому что получения элемента может быть заблокировано до момента, когда данный элемент будет доступен. С другой стороны, добавление элементов в коллекцию всегда работает быстро.