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

Ответ

Конкурентные коллекции — это потокобезопасные структуры данных из пространства имен 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()): Снимок коллекции делается на момент начала перечисления. Изменения, внесенные во время перечисления, в нем не отразятся.

Ответ 18+ 🔞

А, конкурентные коллекции! Ну это ж тема, где без бутылки не разберёшься, если честно. Смотри, в C# есть такие штуки в System.Collections.Concurrent — они созданы специально, чтобы несколько потоков могли в них лазить одновременно, и при этом всё не разъёбывалось в хлам.

В чём прикол, спросишь?

  • Потоки не подерутся: Добавил элемент, удалил, прочитал — всё это происходит так, что один поток другому не наступит на мозги. Атомарно, блядь.
  • Умные замки: Там внутри не тупой lock на всю коллекцию, а хитрая система — точечные блокировки или вообще lock-free алгоритмы, чтобы потоки не стояли в очереди как лохи.
  • Согласованность: Данные внутри не превратятся в кашу, но если один поток что-то запихал, второй может увидеть это не мгновенно. Это называется "слабая согласованность", не пугайся.

Главные герои этой банды:

  1. ConcurrentDictionary<TKey, TValue> Это потокобезопасный словарь, ёпта. Представь кэш в памяти, к которому лезут 10 потоков одновременно — вот для этого он и создан.

    var cache = new ConcurrentDictionary<string, Data>();
    // GetOrAdd — это магия: либо значение достанет, либо создаст новое, и всё это без геморроя с синхронизацией
    var data = cache.GetOrAdd("key1", key => FetchDataFromDb(key));
  2. ConcurrentQueue<T> Очередь по принципу "кто первый зашёл, тот первый вышел". Классика для паттерна "производитель-потребитель": один поток задачи вбрасывает, другой — выгребает и выполняет.

    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). Это две отдельные операции, между ними другой поток может влезть и всё просрать. Используй их встроенные атомарные методы: TryAdd, AddOrUpdate, GetOrAdd — они для этого и придуманы, ебать.
  • Скорость: Если у тебя один поток, то обычный Dictionary или Queue будут быстрее, потому что тут нет накладных расходов на всю эту синхронизационную магию. Не используй их просто так, для красоты.
  • Перебор (GetEnumerator()): Когда начинаешь перебирать, коллекция делает снимок себя на этот момент. Если во время перебора что-то изменится — ты этого в текущем переборе не увидишь. Это не баг, это фича, чтобы голова не взорвалась.