Начиная с версии .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>;
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.
Данный интерфейс обеспечивает унифицированное представление для коллекций производителей/потребителей, чтобы абстракции более высокого уровня, такие как BlockingCollection<T>, могли использовать коллекцию в качестве базового механизма хранения. Интерфейс IProducerConsumerCollection<T> был добавлен в версию .NET 4 для поддержки новых, безопасных в отношении потоков классов коллекций. Существует два основных сценария использования коллекций типа поставщик/потребитель (producer/consumer):
Синтаксис используемый при создании интерфейса представлен ниже:
public interface IProducerConsumerCollection<T> : IEnumerable<T>, ICollection, IEnumerable
где T - Определяет тип элементов коллекции.
Классическим примером использования этого интерфейса являются стеки и очереди. Следующие классы реализуют этот интерфейс:
Интерфейс IProducerConsumerCollection<T> расширяет интерфейс ICollection<T> путем добавления методов и свойств представленных в Табл. 14.2.
| Имя | Описание |
|---|---|
| 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>;
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.
Данный интерфейс обеспечивает унифицированное представление для коллекций производителей/потребителей, чтобы абстракции более высокого уровня, такие как BlockingCollection<T>, могли использовать коллекцию в качестве базового механизма хранения. Интерфейс IProducerConsumerCollection<T> был добавлен в версию .NET 4 для поддержки новых, безопасных в отношении потоков классов коллекций. Существует два основных сценария использования коллекций типа поставщик/потребитель (producer/consumer):
Синтаксис используемый при создании интерфейса представлен ниже:
public interface IProducerConsumerCollection<T> : IEnumerable<T>, ICollection, IEnumerable
где T - Определяет тип элементов коллекции.
Классическим примером использования этого интерфейса являются стеки и очереди. Следующие классы реализуют этот интерфейс:
Интерфейс IProducerConsumerCollection<T> расширяет интерфейс ICollection<T> путем добавления методов и свойств представленных в Табл. 14.2.
| Имя | Описание |
|---|---|
| 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) Ошибка при отработке многопоточной программы
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.