День двести пятьдесят девятый. #ЗаметкиНаПолях

Использование потокобезопасных коллекций. Окончание

BlockingCollection<Т>

С точки зрения разработки лучше всего реализовывать паттерн «производитель/потребитель», то есть рассматривать задачи в многопоточном приложении как либо производителя, либо потребителя данных. Задача, которая и производит, и потребляет данные, уязвима для ситуаций «взаимной блокировки». Если задача A ожидает чего-то, создаваемого задачей B, а задача B ожидает чего-то, создаваемого задачей A, и ни одна из задач не может быть запущена.

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

Следующий код создаёт поток, который пытается добавить 5 элементов в коллекцию BlockingCollection из 3 элементов. После добавления 3-го элемента этот поток блокируется. Программа также создаёт поток, который извлекает элементы из коллекции. Как только поток чтения начинает работать и извлекает некоторые элементы из коллекции, поток записи может продолжиться:

BlockingCollection<int> data = new BlockingCollection<int>(3);

Task.Run(() =>

{

for(int i=1;i<=5;i++)

{

data.Add(i);

Console.WriteLine("Число {0} добавлено.", i);

}

data.CompleteAdding();

});

Console.ReadKey();

Console.WriteLine("Чтение элементов");

Task.Run(() =>

{

while (!data.IsCompleted)

{

try

{

int v = data.Take();

Console.WriteLine("Число {0} прочитано.", v);

}

catch (InvalidOperationException) { }

}

});

Console.ReadKey();




Пример вывода:

Число 1 добавлено

Число 2 добавлено

Число 3 добавлено

Чтение элементов

Число 1 прочитано

Число 2 прочитано

Число 3 прочитано

Число 4 добавлено

Число 5 добавлено

Число 4 прочитано

Число 5 прочитано



Задача добавления вызывает метод CompleteAdding(), после добавления последнего элемента. Это предотвращает добавление новых элементов в коллекцию. Извлекающая задача использует свойство IsCompleted, чтобы определить, когда прекратить получать элементы. Свойство IsCompleted возвращает true, когда коллекция пуста и вызван CompleteAdding. Операция извлечения выполняется внутри конструкции try-catch, т.к. метод Take может выбросить исключение, если между проверкой IsCompleted и вызовом Take добавляющая задача вызовет CompleteAdding. То есть получающая задача попытается выполнить извлечение из коллекции, которая была помечена как завершённая.

BlockingCollection также предоставляет методы TryAdd и TryTake, которые используются для попытки выполнения действия. Они возвращает true, если действие выполнено успешно. Кроме того, им можно передать максимальное время ожидания завершения действия и токен отмены. Класс BlockingCollection выступает в качестве оболочки для других потокобезопасных классов коллекций, включая ConcurrentQueue (используется по умолчанию), ConcurrentStack и ConcurrentBag.



Источник: Rob Miles “Exam Ref 70-483 Programming in C#”. 2nd ed - Pearson Education, Inc., 2019. Глава 1.