Ошибка "Collection was modified; enumeration operation may not execute" в C#: как исправить?
Причина ошибки
Ошибка возникает из-за того, что вы изменяете коллекцию (удаляете элементы) во время её перебора с помощью foreach. Хотя ConcurrentDictionary является потокобезопасным для отдельных операций (добавление, удаление, чтение), он не гарантирует безопасность при итерации, если коллекция модифицируется в том же или другом потоке. В вашем случае фоновый поток удаляет элементы, пока основной поток добавляет новые — это нарушает работу итератора.
Решение: используйте snapshot-итерацию
Чтобы избежать ошибки, нужно создать копию коллекции (snapshot) перед итерацией. Это гарантирует, что вы перебираете фиксированный набор элементов, а модификации происходят с оригиналом. Вот исправленный код:
public class OrderService
{
private ConcurrentDictionary<int, Order> _orders = new ConcurrentDictionary<int, Order>();
public void ProcessOrders()
{
Task.Run(() =>
{
while (true)
{
// Создаём snapshot ключей для безопасной итерации
var keysToRemove = _orders.Keys
.Where(key => _orders.TryGetValue(key, out var order) &&
order.Status == OrderStatus.Completed)
.ToList();
// Удаляем найденные элементы
foreach (var key in keysToRemove)
{
_orders.TryRemove(key, out _);
}
Thread.Sleep(1000);
}
});
for (int i = 0; i < 100; i++)
{
_orders.TryAdd(i, new Order { Status = OrderStatus.Active });
Thread.Sleep(10);
}
}
}
Почему это работает?
- Snapshot через
ToList(): методKeysвозвращает коллекцию ключей, аToList()создаёт её копию в памяти. Теперь вы перебираете копию, а не оригинальный словарь. - Проверка статуса: после создания snapshot мы проверяем статус каждого заказа через
TryGetValue— это потокобезопасно. - Удаление вне итерации: удаление происходит после того, как мы закончили перебор snapshot, поэтому итератор не ломается.
Альтернативные подходы
Если вам нужно удалять элементы прямо во время итерации (например, в цикле while), используйте:
- Ручной обход с индексатором: для списков можно использовать
forс обратным порядком. - Блокировка: оберните итерацию и модификацию в
lock(но это снизит производительность при высокой нагрузке). - Использование
ImmutableDictionary: создавайте новую коллекцию при каждом изменении (подходит для редких обновлений).
Дополнительные советы
- В WCF-сервисах избегайте длительных фоновых задач в экземплярах службы — используйте отдельный сервис или
IHostedService. - Для проверки статусов заказов рассмотрите использование таймеров (
System.Threading.Timer) вместо бесконечного цикла сThread.Sleep. - Логируйте исключения:
ConcurrentDictionaryможет генерироватьArgumentExceptionпри дублировании ключей, хотяTryAddэтого избегает.
Использование блокировки (lock) для синхронизации
Если snapshot-итерация не подходит (например, коллекция очень большая, и вы не хотите тратить память на копию), можно использовать явную синхронизацию через lock. Однако это снижает преимущества ConcurrentDictionary — все потоки будут ждать освобождения блокировки.
private readonly object _lock = new object();
public void ProcessOrders()
{
Task.Run(() =>
{
while (true)
{
lock (_lock)
{
foreach (var kvp in _orders)
{
if (kvp.Value.Status == OrderStatus.Completed)
{
_orders.TryRemove(kvp.Key, out _);
}
}
}
Thread.Sleep(1000);
}
});
for (int i = 0; i < 100; i++)
{
lock (_lock)
{
_orders.TryAdd(i, new Order { Status = OrderStatus.Active });
}
Thread.Sleep(10);
}
}
Когда это оправдано?
- Коллекция относительно небольшая (до тысяч элементов).
- Операции добавления/удаления редкие (например, раз в секунду).
- Вам критична консистентность данных, а не производительность.
Недостаток: при высокой нагрузке (частые вызовы из разных потоков) блокировки могут стать узким местом и снизить пропускную способность сервиса.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги