Что такое потокобезопасная коллекция (коллекция синхронизации)?

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

Ответ

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

Зачем они нужны? При работе с обычными коллекциями (List<T>, Dictionary<TKey, TValue>) из нескольких потоков требуется ручная синхронизация. Потокобезопасные коллекции инкапсулируют эту логику внутри себя, упрощая разработку и снижая риск ошибок (гонок данных, deadlock).

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

Коллекция Описание Аналог из System.Collections.Generic
ConcurrentBag<T> Неупорядоченная коллекция, оптимизированная для сценариев, где один поток добавляет и удаляет данные. List<T> (но без порядка)
ConcurrentQueue<T> Потокобезопасная очередь FIFO (First-In-First-Out). Queue<T>
ConcurrentStack<T> Потокобезопасный стек LIFO (Last-In-First-Out). Stack<T>
ConcurrentDictionary<TKey, TValue> Потокобезопасный словарь. Самый часто используемый тип. Dictionary<TKey, TValue>
BlockingCollection<T> Коллекция с ограниченной емкостью, которая блокирует поток при попытке взять элемент из пустой коллекции или добавить в полную. Реализует шаблон Producer-Consumer. Нет прямого аналога

Пример использования ConcurrentDictionary:

using System.Collections.Concurrent;

// Создание потокобезопасного кеша в памяти
ConcurrentDictionary<string, DateTime> userLastActivityCache = new();

// Метод, который могут вызывать множество потоков одновременно
public void UpdateUserActivity(string userId)
{
    // AddOrUpdate атомарно добавляет или обновляет запись
    userLastActivityCache.AddOrUpdate(
        key: userId,
        addValueFactory: id => DateTime.UtcNow, // Если ключа нет — создаем новую запись
        updateValueFactory: (id, oldValue) => DateTime.UtcNow // Если ключ есть — обновляем
    );

    // Потокобезопасное чтение
    if (userLastActivityCache.TryGetValue(userId, out DateTime lastSeen))
    {
        Console.WriteLine($"User {userId} was last seen at {lastSeen}");
    }
}

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

  • Атомарность операций: Методы типа AddOrUpdate, GetOrAdd в ConcurrentDictionary являются атомарными. Однако последовательность вызовов этих методов уже не атомарна.
  • Итерация: Итерация по коллекции (foreach) выполняется по snapshot (снимку) данных на момент начала перебора. Элементы, добавленные или удаленные во время итерации, не будут отражены.
  • Не для всех сценариев: Для сложных составных операций, требующих транзакционности над несколькими коллекциями, все еще может потребоваться внешняя блокировка (lock).