Ответ
Потокобезопасная коллекция — это структура данных, предназначенная для безопасного использования из нескольких потоков одновременно без необходимости внешней синхронизации с помощью 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).