Основы параллельного программирования с использованием Visual Studio 2010

Параллельные коллекции. Низкоуровневая синхронизация

Разбить на страницы
Показывать лекцию целиком

SpinLock

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

  • Спин-блокировки являются структурами;
  • Спин-блокировки не поддерживают реентерабельность (reentrance), это означает, что нельзя вызвать дважды метод Enter() одного и того же объекта SpinLock в одном и том же потоке, в противном случае - это приведет к генерации исключения (если включено отслеживание владельца (owner tracking)) или к взаимоблокировке (deadlock) (если отслеживание владельца отключено). Можно также включить отслеживание владельца при создании объекта спин-блокировки, однако это приведет к снижению производительности.
  • SpinLock позволяет узнать, захвачена ли блокировка, с помощью свойства IsHeld, и включено ли отслеживание владельца, с помощью свойства IsHeldByCurrentThread.
  • SpinLock, также, отличается от структуры lock тем, что при вызове метода 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 - структуры.

    Основные свойства и методы SpinLock - структуры
    ИмяОписание
    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 также можно использовать самостоятельно для базовых функций цикла в одной программе. При этом следует отметить, что структура SpinWait предназначен для обеспечения производительного параллелизма, сама по себе эта структура не является потокобезопасной, и поэтому каждый поток должен использовать свою копию этой структуры.

    Существует два способа использования структуры SpinWai:

  • Первый способ - использовать метод SpinUntil:
    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.

    Основные свойства и методы SpinWait
    ИмяОписание
    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

    ConcurrentQueue представляет собой потокобезопасную коллекцию, обслуживаемую по принципу "первым поступил - первым обслужен" (FIFO). Этот класс коллекции реализован со свободным от блокировок алгоритмом и использует 32 массива, которые внутренне скомбинированы в связный список. Для доступа к элементам очереди применяются методы, представленные в Табл. 15.3. Имена этих методов схожи с методами коллекции Queue, но с добавлением префикса Try к тем из них, которые могут дать сбой. Поскольку этот класс реализует интерфейс IProducerConsumerCollection, методы TryAdd() и TryTake() аналогичны вызовам методов Enqueue() и TryDequeue().

    Основные методы коллекции ConcurrentQueue
    ИмяОписание
    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

    ConcurrentStack представляет собой потокобезопасную коллекцию, обслуживаемую по принципу "последним поступил - первым обслужен" (LIFO). Данная коллекция похожа на коллекцию ConcurrentQueue, но с другими методами доступа к элементам. Класс ConcurrentStack определяет методы Push(), PushRange(), TryPeek(), TryPop() и TryPopRange(). Более подробно методы класса ConcurrentStack описаны в Табл. 15.4.

    Основные методы коллекции ConcurrentStack
    ИмяОписание
    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

    ConcurrentBag

    Коллекция СoncurrentBag представляет собой потокобезопасную неупорядоченную коллекцию объектов и реализует концепцию отображения потоков на используемые внутренне массивы, и старается избежать блокировок. Для доступа к элементам применяются методы Add(), TryPeek() и TryTake() (Табл. 15.5). СoncurrentBag можно представить как набор очередей с двусторонним доступом (deque). Каждый поток при работе с коллекцией обращается к своей собственной очереди, добавляя и удаляя элементы с ее начала. Когда случается так, что очередь одного потока пуста, а ему нужно извлечь элемент, то он извлекает его из очереди соседнего потока, но уже не сначала очереди, а с противоположного конца очереди. Такой подход позволяет практически не пересекаться разным потокам по данным.

    Основные методы коллекции ConcurrentBag
    ИмяОписание
    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

    Коллекция ConcurrentDictionary представляет собой потокобезопасную коллекцию пар "ключ-значение", доступ к которой могут одновременно получать несколько потоков. Для доступа к элементам коллекции в не блокирующем режиме служат методы TryAdd(), TryGetValue(), TryRemove() и TryUpdate() (Табл. 15.6). Поскольку элементы основаны на ключах и значениях, ConcurrentDictionary<TKey, TVal> не реализует интерфейс IProducerConsumerCollection<T>.

    Основные методы коллекции ConcurrentDictionary
    ИмяОписание
    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, представляет собой потокобезопасную коллекцию, которая осуществляет блокировку и ожидает, пока не появится возможность выполнить действие по добавлению или извлечению элемента. Пример использования коллекции 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. Как показано в примере класс 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.

    Методы для добавления элементов коллекцию BlockingCollection
    ИмяОписание
    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.

    Методы и свойства для сигнализации окончания производства коллекции BlockingCollection
    ИмяОписание
    CompleteAdding() Сигнализирует что производство закончено
    IsAddingComplete Свойство, которое возвращает значение true при вызове метода CompleteAdding()
    IsCompleted Свойство, которое возвращает значение true при вызове метода CompleteAdding() и при отсутствии элементов в коллекции.

    В то время как в цикле, потребитель вызывает метод TryTake() для получения элемента из коллекции. Этот метод возвращает значение true, если элемент был успешно получен и присваивает значение элемента параметру (out deposit). Можно также было использовать Take(), который блокируется, пока элемент коллекции не будет доступен для потребления. Обобщающая информация по данным свойствам и методам представлена в Табл. 15.9.

    Методы и свойства для получения элементов из коллекции BlockingCollection
    ИмяОписание
    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.

    BlockingCollection

  • Создадим консольное приложение и назовем его, к примеру, "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

    В данной части практического задания, будет рассмотрен пример использования параллельной коллекции 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 - структура, которая представляет низкоуровневый взаимоисключающий примитив синхронизации, выполняющий цикл в ожидании получения блокировки. Использование структуры SpinLock аналогично использованию обычной блокировки (lock), но имеет ряд отличий:

  • Спин-блокировки являются структурами;
  • Спин-блокировки не поддерживают реентерабельность (reentrance), это означает, что нельзя вызвать дважды метод Enter() одного и того же объекта SpinLock в одном и том же потоке, в противном случае - это приведет к генерации исключения (если включено отслеживание владельца (owner tracking)) или к взаимоблокировке (deadlock) (если отслеживание владельца отключено). Можно также включить отслеживание владельца при создании объекта спин-блокировки, однако это приведет к снижению производительности.
  • SpinLock позволяет узнать, захвачена ли блокировка, с помощью свойства IsHeld, и включено ли отслеживание владельца, с помощью свойства IsHeldByCurrentThread.
  • SpinLock, также, отличается от структуры lock тем, что при вызове метода 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 - структуры.

    Основные свойства и методы SpinLock - структуры
    ИмяОписание
    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 также можно использовать самостоятельно для базовых функций цикла в одной программе. При этом следует отметить, что структура SpinWait предназначен для обеспечения производительного параллелизма, сама по себе эта структура не является потокобезопасной, и поэтому каждый поток должен использовать свою копию этой структуры.

    Существует два способа использования структуры SpinWai:

  • Первый способ - использовать метод SpinUntil:
    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.

    Основные свойства и методы SpinWait
    ИмяОписание
    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

    ConcurrentQueue представляет собой потокобезопасную коллекцию, обслуживаемую по принципу "первым поступил - первым обслужен" (FIFO). Этот класс коллекции реализован со свободным от блокировок алгоритмом и использует 32 массива, которые внутренне скомбинированы в связный список. Для доступа к элементам очереди применяются методы, представленные в Табл. 15.3. Имена этих методов схожи с методами коллекции Queue, но с добавлением префикса Try к тем из них, которые могут дать сбой. Поскольку этот класс реализует интерфейс IProducerConsumerCollection, методы TryAdd() и TryTake() аналогичны вызовам методов Enqueue() и TryDequeue().

    Основные методы коллекции ConcurrentQueue
    ИмяОписание
    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

    ConcurrentStack представляет собой потокобезопасную коллекцию, обслуживаемую по принципу "последним поступил - первым обслужен" (LIFO). Данная коллекция похожа на коллекцию ConcurrentQueue, но с другими методами доступа к элементам. Класс ConcurrentStack определяет методы Push(), PushRange(), TryPeek(), TryPop() и TryPopRange(). Более подробно методы класса ConcurrentStack описаны в Табл. 15.4.

    Основные методы коллекции ConcurrentStack
    ИмяОписание
    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

    ConcurrentBag

    Коллекция СoncurrentBag представляет собой потокобезопасную неупорядоченную коллекцию объектов и реализует концепцию отображения потоков на используемые внутренне массивы, и старается избежать блокировок. Для доступа к элементам применяются методы Add(), TryPeek() и TryTake() (Табл. 15.5). СoncurrentBag можно представить как набор очередей с двусторонним доступом (deque). Каждый поток при работе с коллекцией обращается к своей собственной очереди, добавляя и удаляя элементы с ее начала. Когда случается так, что очередь одного потока пуста, а ему нужно извлечь элемент, то он извлекает его из очереди соседнего потока, но уже не сначала очереди, а с противоположного конца очереди. Такой подход позволяет практически не пересекаться разным потокам по данным.

    Основные методы коллекции ConcurrentBag
    ИмяОписание
    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

    Коллекция ConcurrentDictionary представляет собой потокобезопасную коллекцию пар "ключ-значение", доступ к которой могут одновременно получать несколько потоков. Для доступа к элементам коллекции в не блокирующем режиме служат методы TryAdd(), TryGetValue(), TryRemove() и TryUpdate() (Табл. 15.6). Поскольку элементы основаны на ключах и значениях, ConcurrentDictionary<TKey, TVal> не реализует интерфейс IProducerConsumerCollection<T>.

    Основные методы коллекции ConcurrentDictionary
    ИмяОписание
    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, представляет собой потокобезопасную коллекцию, которая осуществляет блокировку и ожидает, пока не появится возможность выполнить действие по добавлению или извлечению элемента. Пример использования коллекции 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. Как показано в примере класс 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.

    Методы для добавления элементов коллекцию BlockingCollection
    ИмяОписание
    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.

    Методы и свойства для сигнализации окончания производства коллекции BlockingCollection
    ИмяОписание
    CompleteAdding() Сигнализирует что производство закончено
    IsAddingComplete Свойство, которое возвращает значение true при вызове метода CompleteAdding()
    IsCompleted Свойство, которое возвращает значение true при вызове метода CompleteAdding() и при отсутствии элементов в коллекции.

    В то время как в цикле, потребитель вызывает метод TryTake() для получения элемента из коллекции. Этот метод возвращает значение true, если элемент был успешно получен и присваивает значение элемента параметру (out deposit). Можно также было использовать Take(), который блокируется, пока элемент коллекции не будет доступен для потребления. Обобщающая информация по данным свойствам и методам представлена в Табл. 15.9.

    Методы и свойства для получения элементов из коллекции BlockingCollection
    ИмяОписание
    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.

    BlockingCollection

  • Создадим консольное приложение и назовем его, к примеру, "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

    В данной части практического задания, будет рассмотрен пример использования параллельной коллекции 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();
            }
        }
    }
  • Вернуться к учебному плану