Что такое конкурентные коллекции в .NET?

«Что такое конкурентные коллекции в .NET?» — вопрос из категории Многопоточность, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Конкурентные коллекции — это потокобезопасные структуры данных из пространства имен System.Collections.Concurrent. Они предназначены для безопасного использования в многопоточных сценариях без необходимости ручной синхронизации с помощью lock.

Ключевые особенности:

  • Потокобезопасность: Операции добавления, удаления и чтения являются атомарными.
  • Оптимизированная блокировка: Используют продвинутые техники (fine-grained locking, lock-free алгоритмы) для минимизации конфликтов между потоками.
  • Согласованность: Гарантируют внутреннюю целостность данных, но не гарантируют моментальную видимость изменений для всех потоков (модель слабой согласованности).

Основные типы и их применение:

  1. ConcurrentDictionary<TKey, TValue> Потокобезопасный словарь. Идеален для кэшей или общих справочников.

    var cache = new ConcurrentDictionary<string, Data>();
    // GetOrAdd атомарно получает значение или добавляет новое
    var data = cache.GetOrAdd("key1", key => FetchDataFromDb(key));
  2. ConcurrentQueue<T> Реализует модель FIFO (First-In-First-Out). Часто используется для организации очереди задач между потоками (producer-consumer).

    var taskQueue = new ConcurrentQueue<Action>();
    // Поток-производитель
    taskQueue.Enqueue(() => Console.WriteLine("Task 1"));
    // Поток-потребитель
    if (taskQueue.TryDequeue(out Action task))
    {
        task();
    }
  3. ConcurrentBag<T> Неупорядоченная коллекция, оптимизированная для сценариев, где один поток и добавляет, и удаляет элементы (например, пул объектов).

Важные нюансы:

  • Составные операции: Методы вроде if (!dict.ContainsKey(key)) dict.Add(key, value) не являются атомарными даже с ConcurrentDictionary. Вместо этого используйте атомарные методы типа TryAdd, AddOrUpdate, GetOrAdd.
  • Производительность: В однопоточном режиме они медленнее обычных коллекций (Dictionary, Queue) из-за накладных расходов на синхронизацию. Используйте их только при реальной необходимости в многопоточном доступе.
  • Перечисления (GetEnumerator()): Снимок коллекции делается на момент начала перечисления. Изменения, внесенные во время перечисления, в нем не отразятся.