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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Добавление элементов ("поставка");
  • Получение элемента и его одновременное удаление ("потребление").
  • Синтаксис используемый при создании интерфейса представлен ниже:

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

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

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

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

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

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

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

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

  • В классе ConcurrentStack<T> - метод TryTake() удаляет последний добавленный элемент;
  • В классе ConcurrentQueue<T> - метод TryTake() удаляет самый первый добавленный элемент;
  • В классе ConcurrentBag<T> - метод TryTake() удаляет любой элемент, который может быть удален, максимально эффективно.
  • Эти классы реализуют методы TryTake() и TryAdd(), явно предоставляя ту же самую функциональность с помощью других открытых методов с более точными названиями, такими как TryDequeue() и TryPop().

    Пример (Рис. 14.1) реализации интерфейса IProducerConsumerCollection<T> представлен в примере ниже:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Collections.Concurrent;
    using System.Collections;
    
    namespace IProducerConsumerCollection
    {
        //Реализуем интерфейс IProducerConsumerCollection<T> в классе ProducerConsumerCollection<T>
        class ProducerConsumerCollection<T> : IProducerConsumerCollection<T>
        { 
            private IProducerConsumerCollection<T> _queue = new ConcurrentQueue<T>(); 
            
            public void CopyTo(T[] array, int index)
            {
                Console.WriteLine("Вызов метода CopyTo");
                _queue.CopyTo(array, index);
            }
            public T[] ToArray()
            {
                Console.WriteLine("Вызов метода ToArray");
                return _queue.ToArray();
            }
            public bool TryAdd(T item)
            {
                Console.WriteLine("Вызов метода TryAdd " + item.ToString());
                return _queue.TryAdd(item);
            }
            public bool TryTake(out T item)
            {
                var ret = _queue.TryTake(out item);
                var val = "";
                if (item == null) 
                { val = "NULL"; }
                else
                { val = item.ToString(); }
                Console.WriteLine("Вызов метода TryTake возрвращает объект коллекции " + val + 
       " метод возвращает значение " + ret.ToString());
                return ret;
            }
            public void CopyTo(Array array, int index)
            {
                _queue.CopyTo(array, index);
            }
            public IEnumerator<T> GetEnumerator()
            {
                Console.WriteLine("GetEnumerator T..");
                return _queue.GetEnumerator();
            }
            IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                Console.WriteLine("GetEnumerator..");
                return _queue.GetEnumerator();
            }
            public int Count
            {
                get
                {
                    Console.WriteLine("Вызов метода Count...");
                    return _queue.Count;
                }
            }
            public bool IsSynchronized
            {
                get { return _queue.IsSynchronized; }
            }
            public object SyncRoot
            {
                get { return _queue.SyncRoot; }
            }
        }
        
        class Program
        {
            static void Main(string[] args)
            {
                ProducerConsumerCollection<string> _prodConsumer = new ProducerConsumerCollection<string>(); 
    //создаем объект класса ProducerConsumerCollection
                _prodConsumer.TryAdd("Значение 1 коллекции");//добавляем значения
                _prodConsumer.TryAdd("Значение 2 коллекции");//в коллекцию
                _prodConsumer.TryAdd("Значение 3 коллекции");
                _prodConsumer.TryAdd("Значение 4 коллекции");
                string s;
                _prodConsumer.TryTake(out s);
                _prodConsumer.TryTake(out s);
                var array = _prodConsumer.ToArray();
                for (int n = 0; n < array.Length; n++)
                {
                    Console.WriteLine(array[n]);
                }
                Console.ReadLine();
        
            }
        }
    }
    (рис 14.1) Результат работы программы с использованием интерфейса IProducerConsumerCollection<T>

    Пример использования обычной коллекции с применением параллелизма

    Один из наиболее распространенных способов разделения данных - это использование коллекций. Применение параллельного программирования к коллекциям содержащим большой массив данных: позволит в значительной степени увеличить скорость обработки алгоритма(ов). При этом разработчик может столкнуться с проблемой "соревнования" потоков за доступ к данным коллекции. Пример ниже демонстрирует использование коллекции Queue<int> которая содержит 1000 элементов и элементы которой обрабатывает массив из 10 задач. Каждая задача асинхронно удаляет первый элемент коллекции, и увеличивает значении переменной-счетчика, используя для синхронизации атомарную операцию Interlocked.Increment()

    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)
            {
                // создаем коллекцию
                Queue<int> sharedQueue = new Queue<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)
                        {
                            // удаляем элемент из коллекции с помощью метода Dequeue
                            int item = sharedQueue.Dequeue();
                            // увеливам значение переменной и сохраняем результат
                            Interlocked.Increment(ref itemCount);
                        }
                    });
                    // запускаем новую задачу
                    tasks[i].Start();
                }
                    // ожидаем завершения всех задач
                    Task.WaitAll(tasks);
                    // выводим на экран отчет о количестве обработанных элементов
                    Console.WriteLine("Обработанно элементов: {0}", itemCount);
                     Console.ReadLine();
            }
        }
    }
    (рис 14.2) Результат работы программы с использованием коллекции Queue<int> и многозадачности

    Коллекция Queue<int> не является потокобезапассной, поэтому в случае рассинхронизации потоков (метод Thread.Sleep()), может возникнуть исключение InvalidOperationException (Рис. 14.3). Это исключение возникнет, когда один из потоков попытается вызвать метод Dequeue(), при этом сама коллекция будет уже пустой.

    (рис 14.3) Ошибка при отработке многопоточной программы
    Страницы:

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

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

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

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

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

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

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

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

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

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

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

  • Добавление элементов ("поставка");
  • Получение элемента и его одновременное удаление ("потребление").
  • Синтаксис используемый при создании интерфейса представлен ниже:

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

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

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

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

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

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

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

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

  • В классе ConcurrentStack<T> - метод TryTake() удаляет последний добавленный элемент;
  • В классе ConcurrentQueue<T> - метод TryTake() удаляет самый первый добавленный элемент;
  • В классе ConcurrentBag<T> - метод TryTake() удаляет любой элемент, который может быть удален, максимально эффективно.
  • Эти классы реализуют методы TryTake() и TryAdd(), явно предоставляя ту же самую функциональность с помощью других открытых методов с более точными названиями, такими как TryDequeue() и TryPop().

    Пример (Рис. 14.1) реализации интерфейса IProducerConsumerCollection<T> представлен в примере ниже:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Collections.Concurrent;
    using System.Collections;
    
    namespace IProducerConsumerCollection
    {
        //Реализуем интерфейс IProducerConsumerCollection<T> в классе ProducerConsumerCollection<T>
        class ProducerConsumerCollection<T> : IProducerConsumerCollection<T>
        { 
            private IProducerConsumerCollection<T> _queue = new ConcurrentQueue<T>(); 
            
            public void CopyTo(T[] array, int index)
            {
                Console.WriteLine("Вызов метода CopyTo");
                _queue.CopyTo(array, index);
            }
            public T[] ToArray()
            {
                Console.WriteLine("Вызов метода ToArray");
                return _queue.ToArray();
            }
            public bool TryAdd(T item)
            {
                Console.WriteLine("Вызов метода TryAdd " + item.ToString());
                return _queue.TryAdd(item);
            }
            public bool TryTake(out T item)
            {
                var ret = _queue.TryTake(out item);
                var val = "";
                if (item == null) 
                { val = "NULL"; }
                else
                { val = item.ToString(); }
                Console.WriteLine("Вызов метода TryTake возрвращает объект коллекции " + val + 
       " метод возвращает значение " + ret.ToString());
                return ret;
            }
            public void CopyTo(Array array, int index)
            {
                _queue.CopyTo(array, index);
            }
            public IEnumerator<T> GetEnumerator()
            {
                Console.WriteLine("GetEnumerator T..");
                return _queue.GetEnumerator();
            }
            IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                Console.WriteLine("GetEnumerator..");
                return _queue.GetEnumerator();
            }
            public int Count
            {
                get
                {
                    Console.WriteLine("Вызов метода Count...");
                    return _queue.Count;
                }
            }
            public bool IsSynchronized
            {
                get { return _queue.IsSynchronized; }
            }
            public object SyncRoot
            {
                get { return _queue.SyncRoot; }
            }
        }
        
        class Program
        {
            static void Main(string[] args)
            {
                ProducerConsumerCollection<string> _prodConsumer = new ProducerConsumerCollection<string>(); 
    //создаем объект класса ProducerConsumerCollection
                _prodConsumer.TryAdd("Значение 1 коллекции");//добавляем значения
                _prodConsumer.TryAdd("Значение 2 коллекции");//в коллекцию
                _prodConsumer.TryAdd("Значение 3 коллекции");
                _prodConsumer.TryAdd("Значение 4 коллекции");
                string s;
                _prodConsumer.TryTake(out s);
                _prodConsumer.TryTake(out s);
                var array = _prodConsumer.ToArray();
                for (int n = 0; n < array.Length; n++)
                {
                    Console.WriteLine(array[n]);
                }
                Console.ReadLine();
        
            }
        }
    }
    (рис 14.1) Результат работы программы с использованием интерфейса IProducerConsumerCollection<T>

    Пример использования обычной коллекции с применением параллелизма

    Один из наиболее распространенных способов разделения данных - это использование коллекций. Применение параллельного программирования к коллекциям содержащим большой массив данных: позволит в значительной степени увеличить скорость обработки алгоритма(ов). При этом разработчик может столкнуться с проблемой "соревнования" потоков за доступ к данным коллекции. Пример ниже демонстрирует использование коллекции Queue<int> которая содержит 1000 элементов и элементы которой обрабатывает массив из 10 задач. Каждая задача асинхронно удаляет первый элемент коллекции, и увеличивает значении переменной-счетчика, используя для синхронизации атомарную операцию Interlocked.Increment()

    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)
            {
                // создаем коллекцию
                Queue<int> sharedQueue = new Queue<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)
                        {
                            // удаляем элемент из коллекции с помощью метода Dequeue
                            int item = sharedQueue.Dequeue();
                            // увеливам значение переменной и сохраняем результат
                            Interlocked.Increment(ref itemCount);
                        }
                    });
                    // запускаем новую задачу
                    tasks[i].Start();
                }
                    // ожидаем завершения всех задач
                    Task.WaitAll(tasks);
                    // выводим на экран отчет о количестве обработанных элементов
                    Console.WriteLine("Обработанно элементов: {0}", itemCount);
                     Console.ReadLine();
            }
        }
    }
    (рис 14.2) Результат работы программы с использованием коллекции Queue<int> и многозадачности

    Коллекция Queue<int> не является потокобезапассной, поэтому в случае рассинхронизации потоков (метод Thread.Sleep()), может возникнуть исключение InvalidOperationException (Рис. 14.3). Это исключение возникнет, когда один из потоков попытается вызвать метод Dequeue(), при этом сама коллекция будет уже пустой.

    (рис 14.3) Ошибка при отработке многопоточной программы
    Вернуться к учебному плану