SpinLock - структура, которая представляет низкоуровневый взаимоисключающий примитив синхронизации, выполняющий цикл в ожидании получения блокировки. Использование структуры SpinLock аналогично использованию обычной блокировки (lock), но имеет ряд отличий:
Enter() одного и того же объекта SpinLock в одном и том же потоке, в противном случае - это приведет к генерации исключения (если включено отслеживание владельца (owner tracking)) или к взаимоблокировке (deadlock) (если отслеживание владельца отключено). Можно также включить отслеживание владельца при создании объекта спин-блокировки, однако это приведет к снижению производительности.
IsHeld, и включено ли отслеживание владельца, с помощью свойства IsHeldByCurrentThread.
Enter() используют шаблон для надежной передачи аргумента lockTaken (блок try/finally см. пример). Пример использования шаблона SpinLock приведен ниже:
SpinLock spinLock = new SpinLock (true); // Разрешаем отслеживание владельца
bool lockTaken = false;
try
{
spinLock.Enter (ref lockTaken);
// Какое-то действие
}
finally
{
if (lockTaken) spinLock.Exit();
}
Как и при использовании обычной блокировки (lock), значение булевой переменной lockTaken после вызова метода Enter() будет равным false в случае, если метод сгенерирует исключение и блокировка не будет захвачена. Это происходит в тех случаях, когда вызывается метод Abort() в текущем потоке или генерируется исключение OutOfMemoryException, и позволяет точно знать, нужен ли последующий вызов метода Exit().В Табл. 15.1 представлены основные свойства и методы, SplinLock - структуры.
| Имя | Описание |
|---|---|
IsHeld | Свойство, которое позволяет, получить значение, определяющее, имеет ли какой-либо поток блокировку в настоящий момент. |
IsHeldByCurrentThread | Свойство получает значение, определяющее, имеет ли текущий поток блокировку. |
IsThreadOwnerTrackingEnabled | Свойство получает значение, указывающее, включено ли отслеживание владельца потока для данного экземпляра. |
Enter() | Метод, который получает блокировку надежным способом, то есть даже если в вызове метода возникает исключение, lockTaken можно надежно изучить и определить, была ли получена блокировка. |
Exit(), Exit(Boolean) | Метод, снимающий блокировку. |
TryEnter(Boolean), TryEnter(Int32, Boolean), TryEnter(TimeSpan, Boolean) | Метод пытается получить блокировку надежным способом, то есть даже если в вызове метода возникает исключение, lockTaken можно изучить и определить, была ли получена блокировка. |
Ниже приведен пример использования структуры Spinlock совместно с оператором lock:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
namespace SpinLockExample
{
class Program
{
static int N = 1000000;
static Queue<Data> _queue = new Queue<Data>();
static object _lock = new Object();
static SpinLock _spinlock = new SpinLock();
//создаем класс Data
class Data
{
public string Name { get; set; }
public double Number { get; set; }
}
//создаем метод, добавляющий в коллекцию элементы с использованием структуры SpinLock
private static void UpdateWithSpinLock(Data d, int i)
{
bool lockTaken = false;
try
{
_spinlock.Enter(ref lockTaken);
_queue.Enqueue(d);
}
finally
{
if (lockTaken) _spinlock.Exit(false);
}
}
//создаем метод, выполняющий две одинаковых операции параллельно, которые вызывают метод UpdateWithSpinLock
private static void UseSpinLock()
{
Stopwatch sw = Stopwatch.StartNew(); //создаем объект класса таймер и запускаем его
Parallel.Invoke(
() =>
{
for (int i = 0; i < N; i++)
{
UpdateWithSpinLock(new Data() { Name = i.ToString(), Number = i }, i);
//вызываем метод UpdateWithSpinLock и передаем в него параметры
}
},
() =>
{
for (int i = 0; i < N; i++)
{
UpdateWithSpinLock(new Data() { Name = i.ToString(), Number = i }, i); );
//вызываем метод UpdateWithSpinLock и передаем в него параметры
}
}
);
sw.Stop();//оставливаем таймер
Console.WriteLine("Затраченное время в мс при использовании структуры spinlock:
{0}", sw.ElapsedMilliseconds);//выводим на экран результаты работы таймера в мс
}
//создаем метод, добавляющий в коллекцию элементы с использованием оператора Lock
static void UpdateWithLock(Data d, int i)
{
lock (_lock)
{
_queue.Enqueue(d);
}
}
//создаем метод, выполняющий две одинаковых операции параллельно, которые вызывают метод UseLock
private static void UseLock()
{
Stopwatch sw = Stopwatch.StartNew();//создаем объект класса таймер и запускаем его
Parallel.Invoke(
() =>
{
for (int i = 0; i < N; i++)
{
UpdateWithLock(new Data() { Name = i.ToString(), Number = i }, i); );
//вызываем метод UpdateWithLock и передаем в него параметры
}
},
() =>
{
for (int i = 0; i < N; i++)
{
UpdateWithLock(new Data() { Name = i.ToString(), Number = i }, i);
}
}
); //вызываем метод UpdateWithLock и передаем в него параметры
sw.Stop();//оставливаем таймер
Console.WriteLine("Затраченное время в мс при использовании оператора lock: {0}", sw.ElapsedMilliseconds); )
//выводим на экран результаты работы таймера в мс
}
static void Main(string[] args)
{
UseLock(); //вызываем метод UseLock
_queue.Clear(); //очищаем коллекцию
UseSpinLock();//вызываем метод UseSpinLock Console.ReadLine();
} }}
Как видно из результатов выполнения данного примера (Рис. 15.1), алгоритм использующий структуру Spinlock выполнит операцию быстрее, т.к выполняется минимальный объем работ в критическом фрагменте кода (добавление в коллекцию элементов). Увеличивая объем работы, небольшой объект повышает производительность SpinLock в сравнении со стандартной блокировкой, при этом следует отметить, что SpinLock более ресурсоемкий в отличие от стандартной блокировки.
(рис 15.1) Результат выполнения программы, которая использует для синхронизации структуру SpinLock и оператор lock
SpinWait - это тип синхронизации, который используется в низкоуровневых сценариях, чтобы избежать ресурсоемких переключений контекста и переходов в режим ядра, необходимых для событий ядра. Структуру SpinWait также можно использовать самостоятельно для базовых функций цикла в одной программе. При этом следует отметить, что структура SpinWait предназначен для обеспечения производительного параллелизма, сама по себе эта структура не является потокобезопасной, и поэтому каждый поток должен использовать свою копию этой структуры.
Существует два способа использования структуры SpinWai:
bool _proceed;
void Test()
{
SpinWait.SpinUntil (() => { Thread.MemoryBarrier(); return _proceed; });
}
SpinWait с последующим вызовом метода SpinOnce() в цикле:
bool _proceed;
void Test()
{
var spinWait = new SpinWait();
while (!_proceed) { Thread.MemoryBarrier(); spinWait.SpinOnce(); }
...
}Основные свойства и методы структуры SpinWait представлены в Табл. 15.2.
| Имя | Описание |
|---|---|
Count | Получает число раз, которое SpinOnce был вызван для этого экземпляра. |
NextSpinWillYield | Получает значение, показывающее, даст ли следующий вызов к SpinOnce |
SpinOnce | Выполняет одну прокрутку. |
Reset | Сбрасывает подсчет прокруток. |
SpinUntil (Func<Boolean>) | Выполняет прокрутки до удовлетворения заданного условия. |
Пример использования структуры SpinWait представлен ниже:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace SpinWaitExample
{
class Program
{
static bool someBoolean = false;
static int num = 0;
static void Main()
{
// создаем задачу, которая будет выполняться пока переменной someBoolean
не присвоится значение true
Task t1 = Task.Factory.StartNew(() =>
{
SpinWait sw = new SpinWait();
while (!someBoolean)
{
// NextSpinWillYield возвращает значение true если
// вызов метода sw.SpinOnce()даст доступ к процессору,
// запуская обязательное переключение контекста.
if (sw.NextSpinWillYield) num++;
sw.SpinOnce();
}
Console.WriteLine("SpinWait вызывается {0} раз,
переменная вызывается {1} раз", sw.Count, num);
});
// создаем вторую задачу которая ожидает 100 мс, пока переменной someBoolean
не присвоится значение true
Task t2 = Task.Factory.StartNew(() =>
{
Thread.Sleep(100);
someBoolean = true;
});
// ожидаем выполнение всех задач
Task.WaitAll(t1, t2);
Console.ReadLine();
}
}
(рис 15.2) Результат выполнения программы использующую для синхронизации структуру SpinWait
ConcurrentQueue представляет собой потокобезопасную коллекцию, обслуживаемую по принципу "первым поступил - первым обслужен" (FIFO). Этот класс коллекции реализован со свободным от блокировок алгоритмом и использует 32 массива, которые внутренне скомбинированы в связный список. Для доступа к элементам очереди применяются методы, представленные в Табл. 15.3. Имена этих методов схожи с методами коллекции Queue, но с добавлением префикса Try к тем из них, которые могут дать сбой. Поскольку этот класс реализует интерфейс IProducerConsumerCollection, методы TryAdd() и TryTake() аналогичны вызовам методов Enqueue() и TryDequeue().
| Имя | Описание |
|---|---|
| Enqueue(T) | Добавляет объект в конец коллекции |
| TryPeek(out T) | Пытается удалить и вернуть объект, находящийся в начале коллекции |
| TryDequeue(out T) | Пытается вернуть объект из начала коллекции |
Пример использования коллекции ConcurrentQueue представлен ниже:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Collections;
namespace ConcurrentQueueExample
{
class Program
{
static void Main(string[] args)
{
// создаем коллекцию
ConcurrentQueue<int> sharedQueue = new ConcurrentQueue<int>();
// заполняем коллекцию в цикле с помощью метода Enqueue
for (int i = 0; i < 1000; i++)
{
sharedQueue.Enqueue(i);
}
// объявляем переменную-счетчик количества обработанных элементов
int itemCount = 0;
// создаем список задач
Task[] tasks = new Task[10];
for (int i = 0; i < tasks.Length; i++)
{
// создаем задачу
tasks[i] = new Task(() =>
{
while (sharedQueue.Count > 0)
{
Thread.Sleep(10);
//объявляем переменную для запросов удаления из очереди
int queueElement;
// удаляем элемент из коллекции с помощью метода TryDequeue
bool gotElement = sharedQueue.TryDequeue(out queueElement);
// увеличиваем значение переменной и сохраняем результат
if (gotElement)
{
Interlocked.Increment(ref itemCount);
}
}
}
});
// запускаем новую задачу
tasks[i].Start();
}
// ожидаем завершения всех задач
Task.WaitAll(tasks);
// выводим на экран отчет о количестве обработанных элементов
Console.WriteLine("Обработанно элементов: {0}", itemCount);
Console.ReadLine();
}
}
}
(рис 15.3) Результат выполнения программы использующую коллекцию ConcurrentQueue
ConcurrentStack представляет собой потокобезопасную коллекцию, обслуживаемую по принципу "последним поступил - первым обслужен" (LIFO). Данная коллекция похожа на коллекцию ConcurrentQueue, но с другими методами доступа к элементам. Класс ConcurrentStack определяет методы Push(), PushRange(), TryPeek(), TryPop() и TryPopRange(). Более подробно методы класса ConcurrentStack описаны в Табл. 15.4.
| Имя | Описание |
|---|---|
Push(T) | Добавляет объект в качестве верхнего элемента коллекции. |
PushRange(T[]) | Добавляет неделимым блоком несколько объектов в качестве верхнего элемента коллекции. |
PushRange(T[], int, int) | Добавляет неделимым блоком несколько объектов в качестве верхнего элемента коллекции. |
TryPeek(out T) | Пытается вернуть объект из начала коллекции ConcurrentStack без его удаления. |
TryPop(out T) | Пытается извлечь и вернуть верхний объект коллекции ConcurrentStack. |
TryPopRange(out T[]) | Пытается извлечь и вернуть несколько объектов из начала коллекции ConcurrentStack в виде неделимого блока. |
TryPopRange(out T[], int, int) | Пытается извлечь и вернуть несколько объектов из начала коллекции ConcurrentStack в виде неделимого блока. |
Пример использования коллекции ConcurrentStack представлен ниже:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Collections;
namespace ConcurrentStackExample
{
class Program
{
static void Main(string[] args)
{
// создаем коллекцию
ConcurrentStack<int> sharedStack = new ConcurrentStack<int>();
// заполняем коллекцию в цикле с помощью метода Push
for (int i = 0; i < 1000; i++)
{
sharedStack.Push(i);
}
// объявляем переменную-счетчик количества обработанных элементов
int itemCount = 0;
// создаем список задач
Task[] tasks = new Task[10];
for (int i = 0; i < tasks.Length; i++)
{
// создаем задачу
tasks[i] = new Task(() =>
{
while (sharedStack.Count > 0)
{
Thread.Sleep(10);
int queueElement;
// удаляем элемент из коллекции с помощью метода TryPop
bool gotElement = sharedStack.TryPop(out queueElement);
// увеличиваем значение переменной и сохраняем результат
if (gotElement)
{
Interlocked.Increment(ref itemCount);
}
}
});
// запускаем новую задачу
tasks[i].Start();
}
// ожидаем завершения всех задач
Task.WaitAll(tasks);
// выводим на экран отчет о количестве обработанных элементов
Console.WriteLine("Обработанно элементов: {0}", itemCount);
Console.ReadLine();
}
}
}
(рис 15.4) Результат выполнения программы использующую коллекцию ConcurrentStack
Коллекция СoncurrentBag представляет собой потокобезопасную неупорядоченную коллекцию объектов и реализует концепцию отображения потоков на используемые внутренне массивы, и старается избежать блокировок. Для доступа к элементам применяются методы Add(), TryPeek() и TryTake() (Табл. 15.5). СoncurrentBag можно представить как набор очередей с двусторонним доступом (deque). Каждый поток при работе с коллекцией обращается к своей собственной очереди, добавляя и удаляя элементы с ее начала. Когда случается так, что очередь одного потока пуста, а ему нужно извлечь элемент, то он извлекает его из очереди соседнего потока, но уже не сначала очереди, а с противоположного конца очереди. Такой подход позволяет практически не пересекаться разным потокам по данным.
| Имя | Описание |
|---|---|
Add(T) | Добавляет объект в коллекцию. |
TryPeek(out T) | Пытается вернуть объект из коллекции без его удаления. |
TryTake(out T) | Пытается удалить и вернуть объект из коллекции |
Пример использование коллекции СoncurrentBag представлен ниже:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Collections;
namespace ConcurrentCollection
{
class Program
{
static void Main(string[] args)
{
// создаем коллекцию
ConcurrentBag<int> sharedBag = new ConcurrentBag<int>();
// заполняем коллекцию в цикле с помощью метода Add
for (int i = 0; i < 1000; i++)
{
sharedBag.Add(i);
}
// объявляем переменную-счетчик количества обработанных элементов
int itemCount = 0;
// создаем список задач
Task[] tasks = new Task[10];
for (int i = 0; i < tasks.Length; i++)
{
// создаем задачу
tasks[i] = new Task(() =>
{
while (sharedBag.Count > 0)
{
Thread.Sleep(10);
int queueElement;
// удаляем элемент из коллекции с помощью метода TryTake
bool gotElement = sharedBag.TryTake(out queueElement);
// увеливам значение переменной и сохраняем результат
if (gotElement)
{
Interlocked.Increment(ref itemCount); }
}
});
// запускаем новую задачу
tasks[i].Start();
}
// ожидаем завершения всех задач
Task.WaitAll(tasks);
// выводим на экран отчет о количестве обработанных элементов
Console.WriteLine("Обработанно элементов: {0}", itemCount);
Console.ReadLine();
}
}
}
(рис 15.5) Результат выполнения программы использующую коллекцию СoncurrentBag
Коллекция ConcurrentDictionary представляет собой потокобезопасную коллекцию пар "ключ-значение", доступ к которой могут одновременно получать несколько потоков. Для доступа к элементам коллекции в не блокирующем режиме служат методы TryAdd(), TryGetValue(), TryRemove() и TryUpdate() (Табл. 15.6). Поскольку элементы основаны на ключах и значениях, ConcurrentDictionary<TKey, TVal> не реализует интерфейс IProducerConsumerCollection<T>.
| Имя | Описание |
|---|---|
TryAdd(TKey, TVal) | Пытается добавить указанную пару "ключ-значение" в коллекцию. |
TryGetValue(TKey, out TVal) | Пытается получить значение, связанное с указанным ключом, из коллекции. |
TryRemove(TKey, out TVal) | Пытается удалить и вернуть значение с указанным ключом из коллекции. |
TryUpdate(TKey, TVal, TVal) | Сравнивает существующее значение указанного ключа с заданным значением и в случае их равенства обновляет ключ третьим значением. |
ContainsKey(TKey) | Определяет, содержится ли указанный ключ в словаре |
GetOrAdd (TKey, TVal) | Добавляет пару "ключ значение" в коллекцию, если ключ еще не существует. |
AddOrUpdate (TKey, TVal, Func<TKey, TVal, TVal>) | Добавляет пару "ключ значение" в коллекцию, если данный ключ еще не существует, или обновляет пару "ключ. |
Методы, представленные в Табл. 15.6, следует использовать в следующих случаях:
AddOrUpdate() используют, если необходимо добавить новое значение для заданного ключа, а также в том случае если ключ уже существует и необходимо заменить его значение;
GetOrAdd() используют, если необходимо получить существующее значение для заданного ключа, а также в том случае если ключ не существует и задать значение паре "ключ-значение";
TryAdd(), TryGetValue(), TryUpdate(), TryRemove() используют, если необходимо добавить, получить, обновить или удалить пару "ключ-значение", а также в том случае, если ключ уже существует или попытка завершилась по какой-либо причине ошибкой и необходимо выполнить альтернативные действия. Пример использования коллекции ConcurrentDictionary представлен ниже:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Concurrent;
using System.Threading.Tasks;
namespace ConcurrentDictionaryExample
{
class BankAccount
{
public int Balance
{
get;
set;
}
class Program
{
static void Main(string[] args)
{
// создаем экземпляр банковского счета
BankAccount account = new BankAccount();
// создаем коллекцию
ConcurrentDictionary<object, int> sharedDict
= new ConcurrentDictionary<object, int>();
// создаем список задач, которые возвращают целочисленный массив
Task<int>[] tasks = new Task<int>[10];
for (int i = 0; i < tasks.Length; i++)
{
// помещаем начальные значения в словарь
sharedDict.TryAdd(i, account.Balance);
// создаем новую задачу
tasks[i] = new Task<int>((keyObj) =>
{
// создаем переменную для использования в цикле
int currentValue;
bool gotValue;
// создаем цикл для обновления баланса счета
for (int j = 0; j < 1000; j++)
{
// получаем текущее значение из словаря
gotValue = sharedDict.TryGetValue(keyObj, out currentValue);
// увеличиваем значение и обновляем словарь
sharedDict.TryUpdate(keyObj, currentValue + 1, currentValue);
}
// создаем переменную конечного результата
int result;
// получаем результат из словаря
gotValue = sharedDict.TryGetValue(keyObj, out result);
// возвращаем значение результата, если есть
if (gotValue)
{
return result;
}
else
{
// если нет результата - вызываем исключение
throw new Exception(
String.Format("Нет элементов данных доступных для объекта {0}", keyObj));
}
}, i);
// запускаем задачу
tasks[i].Start();
}
// обновляем баланс счета с помощью результатов выполнения задач
for (int i = 0; i < tasks.Length; i++)
{
account.Balance += tasks[i].Result;
}
// выводим значение счетчика
Console.WriteLine("Ожидаемое значение: {0}, Баланс: {1}",
10000, account.Balance);
Console.ReadLine();
}
}
}
}
(рис 15.6) Результат выполнения программы использующую коллекцию ConcurrentDictionary
Коллекция BlockingCollection, представляет собой потокобезопасную коллекцию, которая осуществляет блокировку и ожидает, пока не появится возможность выполнить действие по добавлению или извлечению элемента. Пример использования коллекции BlockingCollection представлен ниже:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Concurrent;
using System.Threading.Tasks;
namespace BlockingCollectionExample
{
class BankAccount {
public int Balance {
get;
set;
}
}
class Deposit {
public int Amount {
get;
set;
}
}
class Program
{
static void Main(string[] args)
{
// создаем коллекцию BlockingCollection
BlockingCollection<Deposit> blockingCollection
= new BlockingCollection<Deposit>();
// создаем и запускаем задачу, которая будет генерировать депозиты и помещать их в коллекцию
Task[] producers = new Task[3];
for (int i = 0; i < 3; i++) {
producers[i] = Task.Factory.StartNew(() => {
// создаем депозиты
for (int j = 0; j < 20; j++) {
// создаем перевод
Deposit deposit = new Deposit { Amount = 100 };
// помещаем перевод в коллекцию
blockingCollection.Add(deposit);
}
});
};
// создаем продолжение, которое будет сигнализировать об окончании "поставки"
Task.Factory.ContinueWhenAll(producers, antecedents => {
// создаем сигнал - "поставка" окончено
Console.WriteLine("Сигнал об окончании производствки");
blockingCollection.CompleteAdding();
});
// создаем банковский счет
BankAccount account = new BankAccount();
// создаем потребителя, который будет обновлять баланс, основанный на депозитах
Task consumer = Task.Factory.StartNew(() => {
while (!blockingCollection.IsCompleted) {
Deposit deposit;
// пытаемся получить следующий элемент коллекции
if (blockingCollection.TryTake(out deposit)) {
// обновляем баланс с учетом суммы перевода
account.Balance += deposit.Amount;
}
}
// выводим финальный баланс
Console.WriteLine("Итоговый баланс: {0}", account.Balance);
});
consumer.Wait();
Console.ReadLine();
}
}
}
(рис 15.7) Результат выполнения программы использующую коллекцию BlockingCollection
При использовании коллекции BlockingCollection следует:
BlockingCollection, указав при необходимости коллекцию, реализующую интерфейс IProducerConsumerCollection и максимальный размер коллекции.
Первым шагом, как говорилось ранее, является создание экземпляра класса BlockingCollection. Как показано в примере класс BlockingCollection - строго типизирован, и тип соответствует классу Debosit:
// создаем коллекцию BlockingCollection BlockingCollection<Deposit> blockingCollection = new BlockingCollection<Deposit>();
Конструктор по умолчанию использует неограниченный тип коллекции, это означает, что нет ограничений на количество "выдающихся" рабочих элементов в коллекции. Поставщики могут добавлять элементы в коллекцию гораздо быстрее, чем потребители использовать их. Для создания коллекции с ограничением используйте конструктор с аргументом int, как продемонстрировано ниже:
BlockingCollection<Deposit> blockingCollection = new BlockingCollection<Deposit>(5);
Этот пример создает экземпляр класса BlockingCollection, ограниченный пятью элементами, т.е. если поставщик попытается добавить элемент в коллекцию, которая уже будет содержать пять элементов, то метод Add() будет заблокирована, до тех пор, пока потребитель не использует элемент с помощью метода Take().
Следующий шаг - это создание процедуры выполняющую роль поставщика. В нашем примере, создается три задачи, которые создают двадцать экземпляров класса Deposit и добавляют их в коллекцию с помощью метода Add():
Task[] producers = new Task[3];
for (int i = 0; i < 3; i++) {
producers[i] = Task.Factory.StartNew(() => {
// создаем депозиты
for (int j = 0; j < 20; j++) {
// создаем перевод
Deposit deposit = new Deposit { Amount = 100 };
// помещаем перевод в коллекцию
blockingCollection.Add(deposit);
}
});
};
Метод Add() блокируется, до тех пор, пока коллекция BlockingCollection не примет новый элемент данных, т.е если другая задача попытается добавить или взять элемент из коллекции, вызов метода Add() будет заблокирован, пока другая операция, над коллекцией, не будет завершена, это связанно с тем что все операции в BlockingCollection синхронизированы.
Класс BlockingCollection имеет ряд методов для добавления элементов в коллекцию, все эти методы описаны в Табл. 15.7.
| Имя | Описание |
|---|---|
Add(T) | Добавляет элемент в коллекцию |
Add(T, CancellationToken) | Добавляет элемент в коллекцию, контролируя токен отмены. |
TryAdd(T) | Пытается добавить указанный элемент в коллекцию |
TryAdd(T, int) | Пытается добавить указанный элемент в коллекцию в течение указанного временного периода. |
TryAdd(T, TimeSpan) | Пытается добавить указанный элемент в коллекцию, в течение указанного временного периода. |
TryAdd(T, int, CancellationToken) | Пытается добавить указанный элемент в коллекцию в течение указанного временного периода, контролируя токен отмены. |
Использование модели поставщик/потребитель позволяет создать "асимметрию" между поставщиком и потребителем. Использование данной модели необходимо в том случае, если производство и потребление элемента, занимает разное количество времени, Код ниже демонстрирует реализацию потребителя:
Task consumer = Task.Factory.StartNew(() => {
while (!blockingCollection.IsCompleted) {
Deposit deposit;
// пытаемся получить следующий элемент коллекции
if (blockingCollection.TryTake(out deposit)) {
// обновляем баланс с учетом суммы перевода
account.Balance += deposit.Amount;
}
}
// выводим финальный баланс
Console.WriteLine("Итоговый баланс: {0}", account.Balance);
});
Как видно из примера, задача-потребитель входит в цикл, который выполнится в том случае, если свойство BlockingCollection.IsCompleted возвращает значение false. Свойство IsCompleted возвращает значение true, когда вызывается метод CompleteAdding() и коллекция не содержит элементов, это будет означать, что производство и потребление элементов коллекции, было завершено. Обобщающая информация по данным свойствам и методам представлена в Табл. 15.8.
| Имя | Описание |
|---|---|
CompleteAdding() | Сигнализирует что производство закончено |
IsAddingComplete | Свойство, которое возвращает значение true при вызове метода CompleteAdding() |
IsCompleted | Свойство, которое возвращает значение true при вызове метода CompleteAdding() и при отсутствии элементов в коллекции. |
В то время как в цикле, потребитель вызывает метод TryTake() для получения элемента из коллекции. Этот метод возвращает значение true, если элемент был успешно получен и присваивает значение элемента параметру (out deposit). Можно также было использовать Take(), который блокируется, пока элемент коллекции не будет доступен для потребления. Обобщающая информация по данным свойствам и методам представлена в Табл. 15.9.
| Имя | Описание |
|---|---|
Take() | Метод извлекает элемент коллекции. |
Take(CancellationToken) | Метод извлекает элемент коллекции, контролируя указанный токен отмены. |
TryTake(out T) | Метод удаляет элемент из коллекции |
TryTake(out T, int) | Метод удаляет элемент из коллекции в течение указанного временного периода. |
TryTake(out T, TimeSpan) | Метод удаляет элемент из коллекции в течение указанного временного периода. |
TryTake(out T, int, CancellationToken) | Метод удаляет элемент из коллекции в течение указанного временного периода, контролируя токен отмены. |
GetConsumingEnumerable() | Метод удаляет элементы из коллекции с помощью цикла foreach, или ForEach(), или запроса PLINQ. |
В данном практическом задании будет рассмотрено два примера создания консольного приложения с использованием параллельных коллекций BlockingCollection и ConcurrentQueue.
"BlockingCollectionApplication"
(рис P.1)
using System.Threading.Tasks; using System.Threading; using System.Collections.Concurrent;
BlockingCollection<char>:
class Program
{
static BlockingCollection<char> bc;
….
}
Produce(), который "производит" и "поставляет" символы от А до Я:
static void Producer()
{
for (char ch = 'A'; ch <= 'Я'; ch++)
{
bc.Add(ch);
Console.WriteLine("Производится символ " + ch);
}
}
Consumer(), который будет "потреблять" символы метода Produce():
static void Consumer()
{
for (int i = 0; i < 34; i++)
{
Console.WriteLine("Потребляется символ "+bc.Take());
}
}
Примечание. Метод Take удаляет элемент из коллекции.
Main() используем блокирующую коллекцию, ограниченную 4 элементами:
bc = new BlockingCollection<char>(4)
Task Prod = new Task(Producer) Task Con = new Task(Consumer)
Con.Start() Prod.Start()
WaitAll():
try
{
Task.WaitAll(Con, Prod);
}
catch (AggregateException exc)
{
Console.WriteLine(exc);
}
finally
{
Con.Dispose();
Prod.Dispose();
bc.Dispose();
}
В итоге получаем следующее содержание метода Main():
static void Main(string[] args)
{
bc = new BlockingCollection<char>(4);
Task Prod = new Task(Producer);
Task Con = new Task(Consumer);
Con.Start();
Prod.Start();
try
{
Task.WaitAll(Con, Prod);
}
catch (AggregateException exc)
{
Console.WriteLine(exc);
}
finally
{
Con.Dispose();
Prod.Dispose();
bc.Dispose();
}
Console.ReadLine();
}
Produce и Consumer:
(рис P.2)
Смешанный результат объясняется тем, что коллекция ограничена 4-мя элементами, это означает, что в неё может быть добавлено только 4 элемента, прежде чем её придется сократить.
К примеру, если убрать ограничение в четыре символа из коллекции:
bc = new BlockingCollection<char>();
То получится следующий результат:
(рис P.3)
Producer, добавив в него метод CompleteAdding().:
static void Producer()
{
for (char ch = 'А'; ch <= 'Я'; ch++)
{
bc.Add(ch);
Console.WriteLine("Производится символ " + ch);
}
bc.CompleteAdding();
}
Вызов метода CompleteAdding() означает, что в коллекцию не будет добавлено ни одного элемента, это приводит к тому, что свойство IsAddingComplete принимает логическое значение true. Если коллекция пуста, то свойство IsCompleted, принимает логическое значение true, и в этом случае вызовы метода Take() не блокируется.
Consumer, добавив в него метод TryTake.:
static void Consumer()
{
char ch;
while(!bc.IsCompleted)
{
if(bc.TryTake(out ch))
Console.WriteLine("Потребляется символ "+bc.Take());
}
}
Теперь данный метод будет "потреблять" символы до тех пор, пока их будет производить метод-поставщик Consumer().
Примечание. Метод TryTake() возвращает логическое значение true, если элемент коллекции был удален.
(рис P.4)
Данный вариант программы дает такой же результат, как и предыдущий, только с одним отличием: метод Producer() может "производить" и "поставлять" сколько угодно элементов. С этой целью он вызывает метод CompleteAdding() когда завершает создание элементов. Метод Consumer() лишь "потребляет" произведенные элементы до тех пор, пока свойство IsCompleted не примет значение true.
Листинг кода программы:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Concurrent;
namespace BlockingCollectionApplication
{
class Program
{
static BlockingCollection<char> bc;
static void Producer()
{
for (char ch = 'А'; ch <= 'Я'; ch++)
{
bc.Add(ch);
Console.WriteLine("Производится символ " + ch);
}
bc.CompleteAdding();
}
static void Consumer()
{
char ch;
while(!bc.IsCompleted)
{
if(bc.TryTake(out ch))
Console.WriteLine("Потребляется символ "+bc.Take());
}
}
static void Main(string[] args)
{
bc = new BlockingCollection<char>(4);
Task Prod = new Task(Producer);
Task Con = new Task(Consumer);
Con.Start();
Prod.Start();
try
{
Task.WaitAll(Con, Prod);
}
catch (AggregateException exc)
{
Console.WriteLine(exc);
}
finally
{
Con.Dispose();
Prod.Dispose();
bc.Dispose();
}
Console.ReadLine();
}
}
}
В данной части практического задания, будет рассмотрен пример использования параллельной коллекции ConcurrentQueue, при разработке простого консольного приложения.
"ConcurrentQueueApplication":
(рис P.5)
Program, три статичных коллекции:
static List<int> collection1 = new List<int>();
static Queue<int> collection2 = new Queue<int>();
static ArrayList collection3 = new ArrayList();
Предварительно подключив директиву, для использования коллекции типа ArrayList:
using System.Collections
static void addNumber(int num)
{
try
{
collection1.Add(num);
collection2.Enqueue(num);
collection3.Add(num);
}
catch (AggregateException exc)
{
Console.WriteLine(exc);
}
Main, реализуем наполнение коллекций через цикл for с использованием метода addNumber:
for (int i = 0; i < 1000000; i++)
{
addNumber(i);
}
Console.WriteLine("Коллекция List<int> содержит:
{0} элементов \nКоллекция Queue<int> содержит: {1} элементов \nКоллекция ArrayList содержит:
{2} элементов", collection1.Count, collection2.Count, collection3.Count);
Console.ReadLine();
(рис P.6)
Примечание. Как видно из результата выполнения программы все значения были добавлены в коллекции при прохождении цикла for.
Листинг кода программы:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Threading.Tasks;
namespace ConcurrentQueueApplication
{
class Program
{
static List<int> collection1 = new List<int>();
static Queue<int> collection2 = new Queue<int>();
static ArrayList collection3 = new ArrayList();
static void addNumber(int num)
{
try
{
collection1.Add(num);
collection2.Enqueue(num);
collection3.Add(num);
}
catch (Exception exc)
{
Console.WriteLine(exc);
}
}
static void Main(string[] args)
{
for (int i = 0; i < 1000000; i++)
{
addNumber(i);
}
Console.WriteLine("Коллекция List<int> содержит:
{0} элементов \nКоллекция Queue<int> содержит:
{1} элементов \nКоллекция ArrayList содержит: {2} элементов",
collection1.Count, collection2.Count, collection3.Count);
Console.ReadLine();
}
}
}
Parallel.For вместо цикла for для того, что бы посмотреть, как выполняется добавление записей с использованием многопоточного цикла. Для этого, сначала, подключим директиву для использования потоков:
using System.Threading.Tasks
for на Parallel.For:
Parallel.For(0, 1000000, i =>
{
addNumber(i);
});
(рис P.7)
Так же программа может вызвать исключение при добавлении элемента в одну из коллекций:
(рис P.8)
Примечание. Как видно из результатов выполнения цикла Parallel.For, не целесообразно использовать стандартные коллекции для распараллеленного кода, это приводит либо к ошибкам в ходе выполнения программы, либо к тому, что не все элементы будут помещены в коллекцию, это связанно с тем, что несколько потоков могут обращаться к одной и той же позиции коллекции при добавлении элемента.
ConcurrentQueue. Для этого сначала подключим директиву для того, что бы использовать параллельную коллекцию:
using System.Collections.Concurrent
ConcurrentQueue с именем collection4:
static ConcurrentQueue<int> collection4 = new ConcurrentQueue<int>()
collection4.Enqueue(num)
collection4:
Console.WriteLine("Коллекция List<int> содержит:
{0} элементов \nКоллекция Queue<int> содержит:
{1} элементов \nКоллекция ArrayList содержит:
{2} элементов \nКоллекция ConcurrentQueue содержит:
{3} элементов", collection1.Count, collection2.Count, collection3.Count,collection4.Count)
(рис P.9)
Примечание. Как видно из результата выполнения программы, все элементы (от 0 до 1000000) будут помещены в коллекцию ConcurrentQueue.
Листинг кода программы:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Threading.Tasks;
using System.Collections.Concurrent;
namespace ConcurrentQueueApplication
{
class Program
{
static List<int> collection1 = new List<int>();
static Queue<int> collection2 = new Queue<int>();
static ArrayList collection3 = new ArrayList();
static ConcurrentQueue<int> collection4 = new ConcurrentQueue<int>();
static void addNumber(int num)
{
try
{
collection1.Add(num);
collection2.Enqueue(num);
collection3.Add(num);
collection4.Enqueue(num);
}
catch (Exception exc)
{
Console.WriteLine(exc);
}
}
static void Main(string[] args)
{
Parallel.For(0, 1000000, i =>
{
addNumber(i);
});
Console.WriteLine("Коллекция List<int> содержит:
{0} элементов \nКоллекция Queue<int> содержит:
{1} элементов \nКоллекция ArrayList содержит: {2} элементов \nКоллекция ConcurrentQueue содержит:
{3} элементов", collection1.Count, collection2.Count, collection3.Count,collection4.Count);
Console.ReadLine();
}
}
}
SpinLock - структура, которая представляет низкоуровневый взаимоисключающий примитив синхронизации, выполняющий цикл в ожидании получения блокировки. Использование структуры SpinLock аналогично использованию обычной блокировки (lock), но имеет ряд отличий:
Enter() одного и того же объекта SpinLock в одном и том же потоке, в противном случае - это приведет к генерации исключения (если включено отслеживание владельца (owner tracking)) или к взаимоблокировке (deadlock) (если отслеживание владельца отключено). Можно также включить отслеживание владельца при создании объекта спин-блокировки, однако это приведет к снижению производительности.
IsHeld, и включено ли отслеживание владельца, с помощью свойства IsHeldByCurrentThread.
Enter() используют шаблон для надежной передачи аргумента lockTaken (блок try/finally см. пример). Пример использования шаблона SpinLock приведен ниже:
SpinLock spinLock = new SpinLock (true); // Разрешаем отслеживание владельца
bool lockTaken = false;
try
{
spinLock.Enter (ref lockTaken);
// Какое-то действие
}
finally
{
if (lockTaken) spinLock.Exit();
}
Как и при использовании обычной блокировки (lock), значение булевой переменной lockTaken после вызова метода Enter() будет равным false в случае, если метод сгенерирует исключение и блокировка не будет захвачена. Это происходит в тех случаях, когда вызывается метод Abort() в текущем потоке или генерируется исключение OutOfMemoryException, и позволяет точно знать, нужен ли последующий вызов метода Exit().В Табл. 15.1 представлены основные свойства и методы, SplinLock - структуры.
| Имя | Описание |
|---|---|
IsHeld | Свойство, которое позволяет, получить значение, определяющее, имеет ли какой-либо поток блокировку в настоящий момент. |
IsHeldByCurrentThread | Свойство получает значение, определяющее, имеет ли текущий поток блокировку. |
IsThreadOwnerTrackingEnabled | Свойство получает значение, указывающее, включено ли отслеживание владельца потока для данного экземпляра. |
Enter() | Метод, который получает блокировку надежным способом, то есть даже если в вызове метода возникает исключение, lockTaken можно надежно изучить и определить, была ли получена блокировка. |
Exit(), Exit(Boolean) | Метод, снимающий блокировку. |
TryEnter(Boolean), TryEnter(Int32, Boolean), TryEnter(TimeSpan, Boolean) | Метод пытается получить блокировку надежным способом, то есть даже если в вызове метода возникает исключение, lockTaken можно изучить и определить, была ли получена блокировка. |
Ниже приведен пример использования структуры Spinlock совместно с оператором lock:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
namespace SpinLockExample
{
class Program
{
static int N = 1000000;
static Queue<Data> _queue = new Queue<Data>();
static object _lock = new Object();
static SpinLock _spinlock = new SpinLock();
//создаем класс Data
class Data
{
public string Name { get; set; }
public double Number { get; set; }
}
//создаем метод, добавляющий в коллекцию элементы с использованием структуры SpinLock
private static void UpdateWithSpinLock(Data d, int i)
{
bool lockTaken = false;
try
{
_spinlock.Enter(ref lockTaken);
_queue.Enqueue(d);
}
finally
{
if (lockTaken) _spinlock.Exit(false);
}
}
//создаем метод, выполняющий две одинаковых операции параллельно, которые вызывают метод UpdateWithSpinLock
private static void UseSpinLock()
{
Stopwatch sw = Stopwatch.StartNew(); //создаем объект класса таймер и запускаем его
Parallel.Invoke(
() =>
{
for (int i = 0; i < N; i++)
{
UpdateWithSpinLock(new Data() { Name = i.ToString(), Number = i }, i);
//вызываем метод UpdateWithSpinLock и передаем в него параметры
}
},
() =>
{
for (int i = 0; i < N; i++)
{
UpdateWithSpinLock(new Data() { Name = i.ToString(), Number = i }, i); );
//вызываем метод UpdateWithSpinLock и передаем в него параметры
}
}
);
sw.Stop();//оставливаем таймер
Console.WriteLine("Затраченное время в мс при использовании структуры spinlock:
{0}", sw.ElapsedMilliseconds);//выводим на экран результаты работы таймера в мс
}
//создаем метод, добавляющий в коллекцию элементы с использованием оператора Lock
static void UpdateWithLock(Data d, int i)
{
lock (_lock)
{
_queue.Enqueue(d);
}
}
//создаем метод, выполняющий две одинаковых операции параллельно, которые вызывают метод UseLock
private static void UseLock()
{
Stopwatch sw = Stopwatch.StartNew();//создаем объект класса таймер и запускаем его
Parallel.Invoke(
() =>
{
for (int i = 0; i < N; i++)
{
UpdateWithLock(new Data() { Name = i.ToString(), Number = i }, i); );
//вызываем метод UpdateWithLock и передаем в него параметры
}
},
() =>
{
for (int i = 0; i < N; i++)
{
UpdateWithLock(new Data() { Name = i.ToString(), Number = i }, i);
}
}
); //вызываем метод UpdateWithLock и передаем в него параметры
sw.Stop();//оставливаем таймер
Console.WriteLine("Затраченное время в мс при использовании оператора lock: {0}", sw.ElapsedMilliseconds); )
//выводим на экран результаты работы таймера в мс
}
static void Main(string[] args)
{
UseLock(); //вызываем метод UseLock
_queue.Clear(); //очищаем коллекцию
UseSpinLock();//вызываем метод UseSpinLock Console.ReadLine();
} }}
Как видно из результатов выполнения данного примера (Рис. 15.1), алгоритм использующий структуру Spinlock выполнит операцию быстрее, т.к выполняется минимальный объем работ в критическом фрагменте кода (добавление в коллекцию элементов). Увеличивая объем работы, небольшой объект повышает производительность SpinLock в сравнении со стандартной блокировкой, при этом следует отметить, что SpinLock более ресурсоемкий в отличие от стандартной блокировки.
(рис 15.1) Результат выполнения программы, которая использует для синхронизации структуру SpinLock и оператор lock
SpinWait - это тип синхронизации, который используется в низкоуровневых сценариях, чтобы избежать ресурсоемких переключений контекста и переходов в режим ядра, необходимых для событий ядра. Структуру SpinWait также можно использовать самостоятельно для базовых функций цикла в одной программе. При этом следует отметить, что структура SpinWait предназначен для обеспечения производительного параллелизма, сама по себе эта структура не является потокобезопасной, и поэтому каждый поток должен использовать свою копию этой структуры.
Существует два способа использования структуры SpinWai:
bool _proceed;
void Test()
{
SpinWait.SpinUntil (() => { Thread.MemoryBarrier(); return _proceed; });
}
SpinWait с последующим вызовом метода SpinOnce() в цикле:
bool _proceed;
void Test()
{
var spinWait = new SpinWait();
while (!_proceed) { Thread.MemoryBarrier(); spinWait.SpinOnce(); }
...
}Основные свойства и методы структуры SpinWait представлены в Табл. 15.2.
| Имя | Описание |
|---|---|
Count | Получает число раз, которое SpinOnce был вызван для этого экземпляра. |
NextSpinWillYield | Получает значение, показывающее, даст ли следующий вызов к SpinOnce |
SpinOnce | Выполняет одну прокрутку. |
Reset | Сбрасывает подсчет прокруток. |
SpinUntil (Func<Boolean>) | Выполняет прокрутки до удовлетворения заданного условия. |
Пример использования структуры SpinWait представлен ниже:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace SpinWaitExample
{
class Program
{
static bool someBoolean = false;
static int num = 0;
static void Main()
{
// создаем задачу, которая будет выполняться пока переменной someBoolean
не присвоится значение true
Task t1 = Task.Factory.StartNew(() =>
{
SpinWait sw = new SpinWait();
while (!someBoolean)
{
// NextSpinWillYield возвращает значение true если
// вызов метода sw.SpinOnce()даст доступ к процессору,
// запуская обязательное переключение контекста.
if (sw.NextSpinWillYield) num++;
sw.SpinOnce();
}
Console.WriteLine("SpinWait вызывается {0} раз,
переменная вызывается {1} раз", sw.Count, num);
});
// создаем вторую задачу которая ожидает 100 мс, пока переменной someBoolean
не присвоится значение true
Task t2 = Task.Factory.StartNew(() =>
{
Thread.Sleep(100);
someBoolean = true;
});
// ожидаем выполнение всех задач
Task.WaitAll(t1, t2);
Console.ReadLine();
}
}
(рис 15.2) Результат выполнения программы использующую для синхронизации структуру SpinWait
ConcurrentQueue представляет собой потокобезопасную коллекцию, обслуживаемую по принципу "первым поступил - первым обслужен" (FIFO). Этот класс коллекции реализован со свободным от блокировок алгоритмом и использует 32 массива, которые внутренне скомбинированы в связный список. Для доступа к элементам очереди применяются методы, представленные в Табл. 15.3. Имена этих методов схожи с методами коллекции Queue, но с добавлением префикса Try к тем из них, которые могут дать сбой. Поскольку этот класс реализует интерфейс IProducerConsumerCollection, методы TryAdd() и TryTake() аналогичны вызовам методов Enqueue() и TryDequeue().
| Имя | Описание |
|---|---|
| Enqueue(T) | Добавляет объект в конец коллекции |
| TryPeek(out T) | Пытается удалить и вернуть объект, находящийся в начале коллекции |
| TryDequeue(out T) | Пытается вернуть объект из начала коллекции |
Пример использования коллекции ConcurrentQueue представлен ниже:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Collections;
namespace ConcurrentQueueExample
{
class Program
{
static void Main(string[] args)
{
// создаем коллекцию
ConcurrentQueue<int> sharedQueue = new ConcurrentQueue<int>();
// заполняем коллекцию в цикле с помощью метода Enqueue
for (int i = 0; i < 1000; i++)
{
sharedQueue.Enqueue(i);
}
// объявляем переменную-счетчик количества обработанных элементов
int itemCount = 0;
// создаем список задач
Task[] tasks = new Task[10];
for (int i = 0; i < tasks.Length; i++)
{
// создаем задачу
tasks[i] = new Task(() =>
{
while (sharedQueue.Count > 0)
{
Thread.Sleep(10);
//объявляем переменную для запросов удаления из очереди
int queueElement;
// удаляем элемент из коллекции с помощью метода TryDequeue
bool gotElement = sharedQueue.TryDequeue(out queueElement);
// увеличиваем значение переменной и сохраняем результат
if (gotElement)
{
Interlocked.Increment(ref itemCount);
}
}
}
});
// запускаем новую задачу
tasks[i].Start();
}
// ожидаем завершения всех задач
Task.WaitAll(tasks);
// выводим на экран отчет о количестве обработанных элементов
Console.WriteLine("Обработанно элементов: {0}", itemCount);
Console.ReadLine();
}
}
}
(рис 15.3) Результат выполнения программы использующую коллекцию ConcurrentQueue
ConcurrentStack представляет собой потокобезопасную коллекцию, обслуживаемую по принципу "последним поступил - первым обслужен" (LIFO). Данная коллекция похожа на коллекцию ConcurrentQueue, но с другими методами доступа к элементам. Класс ConcurrentStack определяет методы Push(), PushRange(), TryPeek(), TryPop() и TryPopRange(). Более подробно методы класса ConcurrentStack описаны в Табл. 15.4.
| Имя | Описание |
|---|---|
Push(T) | Добавляет объект в качестве верхнего элемента коллекции. |
PushRange(T[]) | Добавляет неделимым блоком несколько объектов в качестве верхнего элемента коллекции. |
PushRange(T[], int, int) | Добавляет неделимым блоком несколько объектов в качестве верхнего элемента коллекции. |
TryPeek(out T) | Пытается вернуть объект из начала коллекции ConcurrentStack без его удаления. |
TryPop(out T) | Пытается извлечь и вернуть верхний объект коллекции ConcurrentStack. |
TryPopRange(out T[]) | Пытается извлечь и вернуть несколько объектов из начала коллекции ConcurrentStack в виде неделимого блока. |
TryPopRange(out T[], int, int) | Пытается извлечь и вернуть несколько объектов из начала коллекции ConcurrentStack в виде неделимого блока. |
Пример использования коллекции ConcurrentStack представлен ниже:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Collections;
namespace ConcurrentStackExample
{
class Program
{
static void Main(string[] args)
{
// создаем коллекцию
ConcurrentStack<int> sharedStack = new ConcurrentStack<int>();
// заполняем коллекцию в цикле с помощью метода Push
for (int i = 0; i < 1000; i++)
{
sharedStack.Push(i);
}
// объявляем переменную-счетчик количества обработанных элементов
int itemCount = 0;
// создаем список задач
Task[] tasks = new Task[10];
for (int i = 0; i < tasks.Length; i++)
{
// создаем задачу
tasks[i] = new Task(() =>
{
while (sharedStack.Count > 0)
{
Thread.Sleep(10);
int queueElement;
// удаляем элемент из коллекции с помощью метода TryPop
bool gotElement = sharedStack.TryPop(out queueElement);
// увеличиваем значение переменной и сохраняем результат
if (gotElement)
{
Interlocked.Increment(ref itemCount);
}
}
});
// запускаем новую задачу
tasks[i].Start();
}
// ожидаем завершения всех задач
Task.WaitAll(tasks);
// выводим на экран отчет о количестве обработанных элементов
Console.WriteLine("Обработанно элементов: {0}", itemCount);
Console.ReadLine();
}
}
}
(рис 15.4) Результат выполнения программы использующую коллекцию ConcurrentStack
Коллекция СoncurrentBag представляет собой потокобезопасную неупорядоченную коллекцию объектов и реализует концепцию отображения потоков на используемые внутренне массивы, и старается избежать блокировок. Для доступа к элементам применяются методы Add(), TryPeek() и TryTake() (Табл. 15.5). СoncurrentBag можно представить как набор очередей с двусторонним доступом (deque). Каждый поток при работе с коллекцией обращается к своей собственной очереди, добавляя и удаляя элементы с ее начала. Когда случается так, что очередь одного потока пуста, а ему нужно извлечь элемент, то он извлекает его из очереди соседнего потока, но уже не сначала очереди, а с противоположного конца очереди. Такой подход позволяет практически не пересекаться разным потокам по данным.
| Имя | Описание |
|---|---|
Add(T) | Добавляет объект в коллекцию. |
TryPeek(out T) | Пытается вернуть объект из коллекции без его удаления. |
TryTake(out T) | Пытается удалить и вернуть объект из коллекции |
Пример использование коллекции СoncurrentBag представлен ниже:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Collections;
namespace ConcurrentCollection
{
class Program
{
static void Main(string[] args)
{
// создаем коллекцию
ConcurrentBag<int> sharedBag = new ConcurrentBag<int>();
// заполняем коллекцию в цикле с помощью метода Add
for (int i = 0; i < 1000; i++)
{
sharedBag.Add(i);
}
// объявляем переменную-счетчик количества обработанных элементов
int itemCount = 0;
// создаем список задач
Task[] tasks = new Task[10];
for (int i = 0; i < tasks.Length; i++)
{
// создаем задачу
tasks[i] = new Task(() =>
{
while (sharedBag.Count > 0)
{
Thread.Sleep(10);
int queueElement;
// удаляем элемент из коллекции с помощью метода TryTake
bool gotElement = sharedBag.TryTake(out queueElement);
// увеливам значение переменной и сохраняем результат
if (gotElement)
{
Interlocked.Increment(ref itemCount); }
}
});
// запускаем новую задачу
tasks[i].Start();
}
// ожидаем завершения всех задач
Task.WaitAll(tasks);
// выводим на экран отчет о количестве обработанных элементов
Console.WriteLine("Обработанно элементов: {0}", itemCount);
Console.ReadLine();
}
}
}
(рис 15.5) Результат выполнения программы использующую коллекцию СoncurrentBag
Коллекция ConcurrentDictionary представляет собой потокобезопасную коллекцию пар "ключ-значение", доступ к которой могут одновременно получать несколько потоков. Для доступа к элементам коллекции в не блокирующем режиме служат методы TryAdd(), TryGetValue(), TryRemove() и TryUpdate() (Табл. 15.6). Поскольку элементы основаны на ключах и значениях, ConcurrentDictionary<TKey, TVal> не реализует интерфейс IProducerConsumerCollection<T>.
| Имя | Описание |
|---|---|
TryAdd(TKey, TVal) | Пытается добавить указанную пару "ключ-значение" в коллекцию. |
TryGetValue(TKey, out TVal) | Пытается получить значение, связанное с указанным ключом, из коллекции. |
TryRemove(TKey, out TVal) | Пытается удалить и вернуть значение с указанным ключом из коллекции. |
TryUpdate(TKey, TVal, TVal) | Сравнивает существующее значение указанного ключа с заданным значением и в случае их равенства обновляет ключ третьим значением. |
ContainsKey(TKey) | Определяет, содержится ли указанный ключ в словаре |
GetOrAdd (TKey, TVal) | Добавляет пару "ключ значение" в коллекцию, если ключ еще не существует. |
AddOrUpdate (TKey, TVal, Func<TKey, TVal, TVal>) | Добавляет пару "ключ значение" в коллекцию, если данный ключ еще не существует, или обновляет пару "ключ. |
Методы, представленные в Табл. 15.6, следует использовать в следующих случаях:
AddOrUpdate() используют, если необходимо добавить новое значение для заданного ключа, а также в том случае если ключ уже существует и необходимо заменить его значение;
GetOrAdd() используют, если необходимо получить существующее значение для заданного ключа, а также в том случае если ключ не существует и задать значение паре "ключ-значение";
TryAdd(), TryGetValue(), TryUpdate(), TryRemove() используют, если необходимо добавить, получить, обновить или удалить пару "ключ-значение", а также в том случае, если ключ уже существует или попытка завершилась по какой-либо причине ошибкой и необходимо выполнить альтернативные действия. Пример использования коллекции ConcurrentDictionary представлен ниже:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Concurrent;
using System.Threading.Tasks;
namespace ConcurrentDictionaryExample
{
class BankAccount
{
public int Balance
{
get;
set;
}
class Program
{
static void Main(string[] args)
{
// создаем экземпляр банковского счета
BankAccount account = new BankAccount();
// создаем коллекцию
ConcurrentDictionary<object, int> sharedDict
= new ConcurrentDictionary<object, int>();
// создаем список задач, которые возвращают целочисленный массив
Task<int>[] tasks = new Task<int>[10];
for (int i = 0; i < tasks.Length; i++)
{
// помещаем начальные значения в словарь
sharedDict.TryAdd(i, account.Balance);
// создаем новую задачу
tasks[i] = new Task<int>((keyObj) =>
{
// создаем переменную для использования в цикле
int currentValue;
bool gotValue;
// создаем цикл для обновления баланса счета
for (int j = 0; j < 1000; j++)
{
// получаем текущее значение из словаря
gotValue = sharedDict.TryGetValue(keyObj, out currentValue);
// увеличиваем значение и обновляем словарь
sharedDict.TryUpdate(keyObj, currentValue + 1, currentValue);
}
// создаем переменную конечного результата
int result;
// получаем результат из словаря
gotValue = sharedDict.TryGetValue(keyObj, out result);
// возвращаем значение результата, если есть
if (gotValue)
{
return result;
}
else
{
// если нет результата - вызываем исключение
throw new Exception(
String.Format("Нет элементов данных доступных для объекта {0}", keyObj));
}
}, i);
// запускаем задачу
tasks[i].Start();
}
// обновляем баланс счета с помощью результатов выполнения задач
for (int i = 0; i < tasks.Length; i++)
{
account.Balance += tasks[i].Result;
}
// выводим значение счетчика
Console.WriteLine("Ожидаемое значение: {0}, Баланс: {1}",
10000, account.Balance);
Console.ReadLine();
}
}
}
}
(рис 15.6) Результат выполнения программы использующую коллекцию ConcurrentDictionary
Коллекция BlockingCollection, представляет собой потокобезопасную коллекцию, которая осуществляет блокировку и ожидает, пока не появится возможность выполнить действие по добавлению или извлечению элемента. Пример использования коллекции BlockingCollection представлен ниже:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Concurrent;
using System.Threading.Tasks;
namespace BlockingCollectionExample
{
class BankAccount {
public int Balance {
get;
set;
}
}
class Deposit {
public int Amount {
get;
set;
}
}
class Program
{
static void Main(string[] args)
{
// создаем коллекцию BlockingCollection
BlockingCollection<Deposit> blockingCollection
= new BlockingCollection<Deposit>();
// создаем и запускаем задачу, которая будет генерировать депозиты и помещать их в коллекцию
Task[] producers = new Task[3];
for (int i = 0; i < 3; i++) {
producers[i] = Task.Factory.StartNew(() => {
// создаем депозиты
for (int j = 0; j < 20; j++) {
// создаем перевод
Deposit deposit = new Deposit { Amount = 100 };
// помещаем перевод в коллекцию
blockingCollection.Add(deposit);
}
});
};
// создаем продолжение, которое будет сигнализировать об окончании "поставки"
Task.Factory.ContinueWhenAll(producers, antecedents => {
// создаем сигнал - "поставка" окончено
Console.WriteLine("Сигнал об окончании производствки");
blockingCollection.CompleteAdding();
});
// создаем банковский счет
BankAccount account = new BankAccount();
// создаем потребителя, который будет обновлять баланс, основанный на депозитах
Task consumer = Task.Factory.StartNew(() => {
while (!blockingCollection.IsCompleted) {
Deposit deposit;
// пытаемся получить следующий элемент коллекции
if (blockingCollection.TryTake(out deposit)) {
// обновляем баланс с учетом суммы перевода
account.Balance += deposit.Amount;
}
}
// выводим финальный баланс
Console.WriteLine("Итоговый баланс: {0}", account.Balance);
});
consumer.Wait();
Console.ReadLine();
}
}
}
(рис 15.7) Результат выполнения программы использующую коллекцию BlockingCollection
При использовании коллекции BlockingCollection следует:
BlockingCollection, указав при необходимости коллекцию, реализующую интерфейс IProducerConsumerCollection и максимальный размер коллекции.
Первым шагом, как говорилось ранее, является создание экземпляра класса BlockingCollection. Как показано в примере класс BlockingCollection - строго типизирован, и тип соответствует классу Debosit:
// создаем коллекцию BlockingCollection BlockingCollection<Deposit> blockingCollection = new BlockingCollection<Deposit>();
Конструктор по умолчанию использует неограниченный тип коллекции, это означает, что нет ограничений на количество "выдающихся" рабочих элементов в коллекции. Поставщики могут добавлять элементы в коллекцию гораздо быстрее, чем потребители использовать их. Для создания коллекции с ограничением используйте конструктор с аргументом int, как продемонстрировано ниже:
BlockingCollection<Deposit> blockingCollection = new BlockingCollection<Deposit>(5);
Этот пример создает экземпляр класса BlockingCollection, ограниченный пятью элементами, т.е. если поставщик попытается добавить элемент в коллекцию, которая уже будет содержать пять элементов, то метод Add() будет заблокирована, до тех пор, пока потребитель не использует элемент с помощью метода Take().
Следующий шаг - это создание процедуры выполняющую роль поставщика. В нашем примере, создается три задачи, которые создают двадцать экземпляров класса Deposit и добавляют их в коллекцию с помощью метода Add():
Task[] producers = new Task[3];
for (int i = 0; i < 3; i++) {
producers[i] = Task.Factory.StartNew(() => {
// создаем депозиты
for (int j = 0; j < 20; j++) {
// создаем перевод
Deposit deposit = new Deposit { Amount = 100 };
// помещаем перевод в коллекцию
blockingCollection.Add(deposit);
}
});
};
Метод Add() блокируется, до тех пор, пока коллекция BlockingCollection не примет новый элемент данных, т.е если другая задача попытается добавить или взять элемент из коллекции, вызов метода Add() будет заблокирован, пока другая операция, над коллекцией, не будет завершена, это связанно с тем что все операции в BlockingCollection синхронизированы.
Класс BlockingCollection имеет ряд методов для добавления элементов в коллекцию, все эти методы описаны в Табл. 15.7.
| Имя | Описание |
|---|---|
Add(T) | Добавляет элемент в коллекцию |
Add(T, CancellationToken) | Добавляет элемент в коллекцию, контролируя токен отмены. |
TryAdd(T) | Пытается добавить указанный элемент в коллекцию |
TryAdd(T, int) | Пытается добавить указанный элемент в коллекцию в течение указанного временного периода. |
TryAdd(T, TimeSpan) | Пытается добавить указанный элемент в коллекцию, в течение указанного временного периода. |
TryAdd(T, int, CancellationToken) | Пытается добавить указанный элемент в коллекцию в течение указанного временного периода, контролируя токен отмены. |
Использование модели поставщик/потребитель позволяет создать "асимметрию" между поставщиком и потребителем. Использование данной модели необходимо в том случае, если производство и потребление элемента, занимает разное количество времени, Код ниже демонстрирует реализацию потребителя:
Task consumer = Task.Factory.StartNew(() => {
while (!blockingCollection.IsCompleted) {
Deposit deposit;
// пытаемся получить следующий элемент коллекции
if (blockingCollection.TryTake(out deposit)) {
// обновляем баланс с учетом суммы перевода
account.Balance += deposit.Amount;
}
}
// выводим финальный баланс
Console.WriteLine("Итоговый баланс: {0}", account.Balance);
});
Как видно из примера, задача-потребитель входит в цикл, который выполнится в том случае, если свойство BlockingCollection.IsCompleted возвращает значение false. Свойство IsCompleted возвращает значение true, когда вызывается метод CompleteAdding() и коллекция не содержит элементов, это будет означать, что производство и потребление элементов коллекции, было завершено. Обобщающая информация по данным свойствам и методам представлена в Табл. 15.8.
| Имя | Описание |
|---|---|
CompleteAdding() | Сигнализирует что производство закончено |
IsAddingComplete | Свойство, которое возвращает значение true при вызове метода CompleteAdding() |
IsCompleted | Свойство, которое возвращает значение true при вызове метода CompleteAdding() и при отсутствии элементов в коллекции. |
В то время как в цикле, потребитель вызывает метод TryTake() для получения элемента из коллекции. Этот метод возвращает значение true, если элемент был успешно получен и присваивает значение элемента параметру (out deposit). Можно также было использовать Take(), который блокируется, пока элемент коллекции не будет доступен для потребления. Обобщающая информация по данным свойствам и методам представлена в Табл. 15.9.
| Имя | Описание |
|---|---|
Take() | Метод извлекает элемент коллекции. |
Take(CancellationToken) | Метод извлекает элемент коллекции, контролируя указанный токен отмены. |
TryTake(out T) | Метод удаляет элемент из коллекции |
TryTake(out T, int) | Метод удаляет элемент из коллекции в течение указанного временного периода. |
TryTake(out T, TimeSpan) | Метод удаляет элемент из коллекции в течение указанного временного периода. |
TryTake(out T, int, CancellationToken) | Метод удаляет элемент из коллекции в течение указанного временного периода, контролируя токен отмены. |
GetConsumingEnumerable() | Метод удаляет элементы из коллекции с помощью цикла foreach, или ForEach(), или запроса PLINQ. |
В данном практическом задании будет рассмотрено два примера создания консольного приложения с использованием параллельных коллекций BlockingCollection и ConcurrentQueue.
"BlockingCollectionApplication"
(рис P.1)
using System.Threading.Tasks; using System.Threading; using System.Collections.Concurrent;
BlockingCollection<char>:
class Program
{
static BlockingCollection<char> bc;
….
}
Produce(), который "производит" и "поставляет" символы от А до Я:
static void Producer()
{
for (char ch = 'A'; ch <= 'Я'; ch++)
{
bc.Add(ch);
Console.WriteLine("Производится символ " + ch);
}
}
Consumer(), который будет "потреблять" символы метода Produce():
static void Consumer()
{
for (int i = 0; i < 34; i++)
{
Console.WriteLine("Потребляется символ "+bc.Take());
}
}
Примечание. Метод Take удаляет элемент из коллекции.
Main() используем блокирующую коллекцию, ограниченную 4 элементами:
bc = new BlockingCollection<char>(4)
Task Prod = new Task(Producer) Task Con = new Task(Consumer)
Con.Start() Prod.Start()
WaitAll():
try
{
Task.WaitAll(Con, Prod);
}
catch (AggregateException exc)
{
Console.WriteLine(exc);
}
finally
{
Con.Dispose();
Prod.Dispose();
bc.Dispose();
}
В итоге получаем следующее содержание метода Main():
static void Main(string[] args)
{
bc = new BlockingCollection<char>(4);
Task Prod = new Task(Producer);
Task Con = new Task(Consumer);
Con.Start();
Prod.Start();
try
{
Task.WaitAll(Con, Prod);
}
catch (AggregateException exc)
{
Console.WriteLine(exc);
}
finally
{
Con.Dispose();
Prod.Dispose();
bc.Dispose();
}
Console.ReadLine();
}
Produce и Consumer:
(рис P.2)
Смешанный результат объясняется тем, что коллекция ограничена 4-мя элементами, это означает, что в неё может быть добавлено только 4 элемента, прежде чем её придется сократить.
К примеру, если убрать ограничение в четыре символа из коллекции:
bc = new BlockingCollection<char>();
То получится следующий результат:
(рис P.3)
Producer, добавив в него метод CompleteAdding().:
static void Producer()
{
for (char ch = 'А'; ch <= 'Я'; ch++)
{
bc.Add(ch);
Console.WriteLine("Производится символ " + ch);
}
bc.CompleteAdding();
}
Вызов метода CompleteAdding() означает, что в коллекцию не будет добавлено ни одного элемента, это приводит к тому, что свойство IsAddingComplete принимает логическое значение true. Если коллекция пуста, то свойство IsCompleted, принимает логическое значение true, и в этом случае вызовы метода Take() не блокируется.
Consumer, добавив в него метод TryTake.:
static void Consumer()
{
char ch;
while(!bc.IsCompleted)
{
if(bc.TryTake(out ch))
Console.WriteLine("Потребляется символ "+bc.Take());
}
}
Теперь данный метод будет "потреблять" символы до тех пор, пока их будет производить метод-поставщик Consumer().
Примечание. Метод TryTake() возвращает логическое значение true, если элемент коллекции был удален.
(рис P.4)
Данный вариант программы дает такой же результат, как и предыдущий, только с одним отличием: метод Producer() может "производить" и "поставлять" сколько угодно элементов. С этой целью он вызывает метод CompleteAdding() когда завершает создание элементов. Метод Consumer() лишь "потребляет" произведенные элементы до тех пор, пока свойство IsCompleted не примет значение true.
Листинг кода программы:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Concurrent;
namespace BlockingCollectionApplication
{
class Program
{
static BlockingCollection<char> bc;
static void Producer()
{
for (char ch = 'А'; ch <= 'Я'; ch++)
{
bc.Add(ch);
Console.WriteLine("Производится символ " + ch);
}
bc.CompleteAdding();
}
static void Consumer()
{
char ch;
while(!bc.IsCompleted)
{
if(bc.TryTake(out ch))
Console.WriteLine("Потребляется символ "+bc.Take());
}
}
static void Main(string[] args)
{
bc = new BlockingCollection<char>(4);
Task Prod = new Task(Producer);
Task Con = new Task(Consumer);
Con.Start();
Prod.Start();
try
{
Task.WaitAll(Con, Prod);
}
catch (AggregateException exc)
{
Console.WriteLine(exc);
}
finally
{
Con.Dispose();
Prod.Dispose();
bc.Dispose();
}
Console.ReadLine();
}
}
}
В данной части практического задания, будет рассмотрен пример использования параллельной коллекции ConcurrentQueue, при разработке простого консольного приложения.
"ConcurrentQueueApplication":
(рис P.5)
Program, три статичных коллекции:
static List<int> collection1 = new List<int>();
static Queue<int> collection2 = new Queue<int>();
static ArrayList collection3 = new ArrayList();
Предварительно подключив директиву, для использования коллекции типа ArrayList:
using System.Collections
static void addNumber(int num)
{
try
{
collection1.Add(num);
collection2.Enqueue(num);
collection3.Add(num);
}
catch (AggregateException exc)
{
Console.WriteLine(exc);
}
Main, реализуем наполнение коллекций через цикл for с использованием метода addNumber:
for (int i = 0; i < 1000000; i++)
{
addNumber(i);
}
Console.WriteLine("Коллекция List<int> содержит:
{0} элементов \nКоллекция Queue<int> содержит: {1} элементов \nКоллекция ArrayList содержит:
{2} элементов", collection1.Count, collection2.Count, collection3.Count);
Console.ReadLine();
(рис P.6)
Примечание. Как видно из результата выполнения программы все значения были добавлены в коллекции при прохождении цикла for.
Листинг кода программы:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Threading.Tasks;
namespace ConcurrentQueueApplication
{
class Program
{
static List<int> collection1 = new List<int>();
static Queue<int> collection2 = new Queue<int>();
static ArrayList collection3 = new ArrayList();
static void addNumber(int num)
{
try
{
collection1.Add(num);
collection2.Enqueue(num);
collection3.Add(num);
}
catch (Exception exc)
{
Console.WriteLine(exc);
}
}
static void Main(string[] args)
{
for (int i = 0; i < 1000000; i++)
{
addNumber(i);
}
Console.WriteLine("Коллекция List<int> содержит:
{0} элементов \nКоллекция Queue<int> содержит:
{1} элементов \nКоллекция ArrayList содержит: {2} элементов",
collection1.Count, collection2.Count, collection3.Count);
Console.ReadLine();
}
}
}
Parallel.For вместо цикла for для того, что бы посмотреть, как выполняется добавление записей с использованием многопоточного цикла. Для этого, сначала, подключим директиву для использования потоков:
using System.Threading.Tasks
for на Parallel.For:
Parallel.For(0, 1000000, i =>
{
addNumber(i);
});
(рис P.7)
Так же программа может вызвать исключение при добавлении элемента в одну из коллекций:
(рис P.8)
Примечание. Как видно из результатов выполнения цикла Parallel.For, не целесообразно использовать стандартные коллекции для распараллеленного кода, это приводит либо к ошибкам в ходе выполнения программы, либо к тому, что не все элементы будут помещены в коллекцию, это связанно с тем, что несколько потоков могут обращаться к одной и той же позиции коллекции при добавлении элемента.
ConcurrentQueue. Для этого сначала подключим директиву для того, что бы использовать параллельную коллекцию:
using System.Collections.Concurrent
ConcurrentQueue с именем collection4:
static ConcurrentQueue<int> collection4 = new ConcurrentQueue<int>()
collection4.Enqueue(num)
collection4:
Console.WriteLine("Коллекция List<int> содержит:
{0} элементов \nКоллекция Queue<int> содержит:
{1} элементов \nКоллекция ArrayList содержит:
{2} элементов \nКоллекция ConcurrentQueue содержит:
{3} элементов", collection1.Count, collection2.Count, collection3.Count,collection4.Count)
(рис P.9)
Примечание. Как видно из результата выполнения программы, все элементы (от 0 до 1000000) будут помещены в коллекцию ConcurrentQueue.
Листинг кода программы:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Threading.Tasks;
using System.Collections.Concurrent;
namespace ConcurrentQueueApplication
{
class Program
{
static List<int> collection1 = new List<int>();
static Queue<int> collection2 = new Queue<int>();
static ArrayList collection3 = new ArrayList();
static ConcurrentQueue<int> collection4 = new ConcurrentQueue<int>();
static void addNumber(int num)
{
try
{
collection1.Add(num);
collection2.Enqueue(num);
collection3.Add(num);
collection4.Enqueue(num);
}
catch (Exception exc)
{
Console.WriteLine(exc);
}
}
static void Main(string[] args)
{
Parallel.For(0, 1000000, i =>
{
addNumber(i);
});
Console.WriteLine("Коллекция List<int> содержит:
{0} элементов \nКоллекция Queue<int> содержит:
{1} элементов \nКоллекция ArrayList содержит: {2} элементов \nКоллекция ConcurrentQueue содержит:
{3} элементов", collection1.Count, collection2.Count, collection3.Count,collection4.Count);
Console.ReadLine();
}
}
}
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.