Исключение InvalidOperationException: коллекция была изменена — несмотря на блокировку коллекции

#c# #multithreading #collections

#c# #многопоточность #Коллекции

Вопрос:

У меня есть синхронизированная хэш-таблица, из которой я регулярно удаляю некоторые записи. Этот код выполняется несколькими потоками. Итак, я блокирую весь foreach, но я все еще иногда получаю исключение InvalidOperationException: коллекция была изменена … в Hashtable.HashtableEnumerator.MoveNext() — т.е. в цикле foreach. Что я делаю не так? Недостаточно блокировки?

 private static readonly Hashtable sessionsTimeoutData = Hashtable.Synchronized(new Hashtable(5000));

private static void ClearTimedoutSessions() { List keysToRemove = new List(); long now = DateTime.Now.Ticks; lock (sessionsTimeoutData) { TimeoutData timeoutData; foreach (DictionaryEntry entry in sessionsTimeoutData) { timeoutData = (TimeoutData)entry.Value; if (now - timeoutData.LastAccessTime > timeoutData.UserTimeoutTicks) keysToRemove.Add((ulong)entry.Key); } } foreach (ulong key in keysToRemove) sessionsTimeoutData.Remove(key); }

Комментарии:

1. Совет профессионала: если вы не застряли в .NET 1.1, используйте общие версии. Они быстрее, безопаснее и качественнее.

Ответ №1:

Вы хотите заблокировать использование, SyncRoot который является объектом, на котором будут блокироваться методы для синхронизированного Hashtable :

 lock (sessionsTimeoutData.SyncRoot)
{
    // ...
}
  

Смотрите http://msdn.microsoft.com/en-us/library/system.collections.хэш-таблица.synchronized.aspx:

Перечисление через коллекцию по сути не является потокобезопасной процедурой. Даже когда коллекция синхронизирована, другие потоки все еще могут изменять коллекцию, что заставляет перечислитель выдавать исключение. Чтобы гарантировать потокобезопасность во время перечисления, вы можете либо заблокировать коллекцию во время всего перечисления, либо перехватить исключения, возникающие в результате изменений, внесенных другими потоками.

Следующий пример кода показывает, как заблокировать коллекцию с помощью SyncRoot на протяжении всего перечисления:

 Hashtable myCollection = new Hashtable();
lock(myCollection.SyncRoot)
{
    foreach (object item in myCollection)
    {
        // Insert your code here.
    }
}
  

Комментарии:

1. Это хорошая идея, но реальная проблема в том, что второй foreach (тот, который удаляет данные) находится за пределами блокировки.

2. @Etienne: каждый Remove() вызов будет синхронизирован (будет принимать внутреннюю блокировку). Коллекция останется согласованной — обновления коллекции могут выполняться между удалениями, но каждый Remove() вызов будет в порядке. В зависимости от поведения приложения возможно, что у вас может возникнуть гонка, в которой объект с этим ключом заменяется другим потоком, и в зависимости от потребностей приложения может быть некорректно удалять обновленную пару ключ / значение. Но это другая проблема, чем та, с которой сталкивается операционная система.

Ответ №2:

Почему второй foreach находится за пределами блокировки?

Комментарии:

1. Я не понимаю, почему это должно быть внутри блокировки. Меня беспокоит только то, что когда я перечисляю хэш-таблицу, никакой другой поток не может ее изменить.

Ответ №3:

Вам нужно заблокировать во время удаления, а также во время вычисления того, что нужно удалить. Переместите это,

 foreach (ulong key in keysToRemove)
        sessionsTimeoutData.Remove(key);
  

В вашем заблокированном разделе.