Преимущества использования потокобезопасных коллекций
В .NET Framework 4 появилось пять типов коллекций, специально разработанных для поддержки многопотоковых операций добавления и удаления. Для достижения потокобезопасности эти типы используют различные виды эффективных механизмов синхронизации с блокировкой и без нее. Синхронизация добавляет к операции издержки. Значения издержек зависят от используемого типа синхронизации, выполняемого типа операции и других факторов, например количества потоков, которые одновременно пытаются получить доступ к коллекции.
В некоторых сценариях издержки синхронизации незначительны и позволяют многопотоковым вариантам выполняться значительно быстрее и обеспечивают лучшую масштабируемость, чем в случае потоконебезопасного эквивалента при защите с помощью внешней блокировки. В других сценариях издержки могут вызвать ситуацию, когда потокобезопасный вариант выполняется и масштабируется примерно так же и даже более медленно, чем потоконебезопасная версия типа с внешней блокировкой.
В следующих подразделах приводятся общие рекомендации по использованию потокобезопасной коллекции и потоконебезопасного эквивалента, который содержит заданную пользователем блокировку для операций чтения и записи. Так как производительность может зависеть от множества факторов, рекомендации нехарактерны и необязательно являются допустимыми во всех обстоятельствах. Если производительность имеет важное значение, то лучшим способом для определения используемого типа коллекции является измерение производительности на основе обычной конфигурации компьютера и нагрузке. В данном документе используются следующие термины.
Чистый сценарий "производитель — потребитель"
Все заданные потоки либо добавляют элементы, либо удаляют их, но не то и другое одновременно.
Смешанный сценарий "производитель — потребитель"
Все заданные потоки как добавляют элементы, так и удаляют их.
Ускорение
Ускорение производительности алгоритма одного типа относительно другого типа в рамках одного сценария.
Масштабируемость
Увеличение в производительности, которое пропорционально числу ядер в компьютере. Масштабируемый алгоритм выполняется быстрее на компьютере, у которого восемь ядер, чем на компьютере, у которого два ядра.
ConcurrentQueue(T) и Queue(T)
В чистых сценариях "производитель-получатель", когда время обработки каждого элемента очень мало (несколько инструкций), класс System.Collections.Concurrent.ConcurrentQueue<T> может дать незначительный рост производительности по сравнению с классом System.Collections.Generic.Queue<T>, который использует внешнюю блокировку. В этом сценарии класс ConcurrentQueue<T> выполняется лучше, когда один выделенный поток помещается в очередь, а другой выделенный поток удаляется из очереди. Если это правило не применяется, класс Queue<T> может даже выполняться немного быстрее, чем класс ConcurrentQueue<T> на компьютерах с многоядерными процессорами.
Когда время обработки составляет 500 FLOPS (операций с плавающей запятой) или больше, то правило двух потоков не применяется к классу ConcurrentQueue<T>, который имеет очень хорошую масштабируемость. Queue<T> в этой ситуации не обладает хорошей масштабируемостью.
В смешанных сценариях "производитель-получатель", когда время обработки очень мало, класс Queue<T>, который имеет внешнюю блокировку, масштабируется лучше, чем классConcurrentQueue<T>. Однако, если время обработки имеет значение приблизительно равное 500 FLOPS и выше, то класс ConcurrentQueue<T> масштабируется лучше.
ConcurrentStack и Stack
В чистых сценариях "производитель-получатель", когда время обработки каждого элемента очень мало, класс System.Collections.Concurrent.ConcurrentStack<T> и класс System.Collections.Generic.Stack<T>, который использует внешнюю блокировку, обычно выполняются с одинаковой скоростью при одном выделенном потоке на добавление и одном выделенном потоке на извлечение. Однако по мере увеличения числа потоков производительность снижается у обоих типов, так как увеличивается число конфликтных ситуаций, и класс Stack<T> может выполняться лучше, чем класс ConcurrentStack<T>. Если время обработки имеет значение приблизительно равное 500 FLOPS и выше, то оба типа масштабируются примерно одинаково.
В смешанных сценариях "производитель-получатель" класс ConcurrentStack<T> имеет большее ускорение для небольших и больших рабочих нагрузок.
Использование методов PushRange и TryPopRange может значительно снизить время доступа.
ConcurrentDictionary и Dictionary
Как правило, лучше использовать класс System.Collections.Concurrent.ConcurrentDictionary<TKey,TValue> в любой ситуации, когда вы одновременно добавляете и обновляете ключи или значения из множества потоков. В сценариях, которые включают частные операции обновления и относительно редкие операции чтения, класс ConcurrentDictionary<TKey,TValue>, в общем случае, обеспечивает немного лучшую производительность. В сценариях, которые включают частные операции чтения и относительно редкие операции обновления, класс ConcurrentDictionary<TKey,TValue>, в общем случае, имеет значительно большее ускорение на компьютерах с многоядерными процессорами.
В сценариях, которые включают частые обновления, можно увеличить степень параллелизма в классе ConcurrentDictionary<TKey,TValue> и затем провести оценку, чтобы увидеть, увеличилась ли производительность на компьютерах с многоядерными процессорами. При изменении уровня параллелизма исключите, насколько это возможно, глобальные операции.
Если выполняются только операции чтения ключа или значений, класс Dictionary<TKey,TValue> работает быстрее, так как он не требует синхронизации, пока словарь не изменяется каким-либо потоком.
ConcurrentBag
В чистых сценариях "производитель — получатель" класс System.Collections.Concurrent.ConcurrentBag<T> может выполняться более медленно, чем другие типы параллельных коллекций.
В смешанных сценариях "производитель-получатель" класс ConcurrentBag<T> в общем случае имеет большее ускорение и большую масштабируемость, чем все остальные типы параллельных коллекций для небольших и больших рабочих нагрузок.
BlockingCollection
Если вы хотите использовать семантику границ и блокировок, класс System.Collections.Concurrent.BlockingCollection<T> может работать быстрее, чем любые пользовательские реализации. Он также поддерживает гибкую обработку исключений и операций отмены, перечисления.