BackgroundWorker.RunWorkerCompleted исключение в List.Add
У нас есть некоторый код, который создает несколько потоков BackgroundWorker, каждый из которых выполняет некоторые функции базы данных. Иногда эти потоки выдают исключение (обычно из-за тайм-аута, что является недавним событием, и я не тот парень, который должен это выяснить).
Если какие-либо потоки терпят неудачу, вся операция бесполезна, и все это происходит в вызове веб-службы. Таким образом, в случае сбоя нам нужно создать исключение в главном потоке, которое будет перехвачено и преобразовано в исключение сбоя SOAP для клиента.
Мы собираем исключения потоков в список. Из десятков раз, когда этот код выполнялся с семью рабочими потоками, каждый из которых генерировал исключения в одно и то же время, один раз List вызывал исключение в System.Collections.Generic.List`1.Add(T item):
System.IndexOutOfRangeException
Message: Index was outside the bounds of the array.
Вот, примерно, код:
// Collect Exceptions thrown by async calls.
var exAsync = new List<Exception>();
int ctThreadsFinished = 0;
int ctThreadsBegun = 0;
Action<Exception> handleException = (ex) => {
lock(exAsync) {
++ctThreadsFinished;
exAsync.Add(ex);
}
};
// ...create and run multiple BackgroundWorker threads, incrementing
// ctThreadsBegun for each thread. They will ++ctThreadsFinished on
// successful completion. That part works.
// If a thread throws an exception, its RunWorkerCompleted event will pass the
// exception to handleException.
while (ctThreadsFinished < ctThreadsBegun)
{
System.Threading.Thread.Sleep(100);
}
if (exAsync.Count == 1)
{
throw new Exception(exAsync.First().Message, exAsync.First());
}
else if (exAsync.Count > 1)
{
var msg = String.Join("\n", exAsync.Select(ex => ex.Message));
throw new AggregateException(msg, exAsync);
}
Я поставил на него блокировку, потому что я предполагал, что RunWorkerCompleted был вызван в рабочем потоке ( обычно это не так, но это веб-служба, и похоже, что поведение вне приложения Windows будет отличаться).
Исключение выглядит как List.Add вызывается потоком 1, затем вызывается потоком 2, пока первый вызов все еще выполняется, а объект все еще находится в несогласованном состоянии. Поскольку множественные сбои всегда (на практике, пока) из-за того, что несколько потоков превышают 30-секундный тайм-аут SqlCommand по умолчанию, они будут делать это в одно и то же время. И я могу воссоздать именно такое поведение в небольшом тестовом приложении, если в списке нет блокировки.
Может ли быть так, что он увеличивает ctThreadsFinished перед Add в нужный момент, чтобы пройти цикл ожидания, поэтому он обращается к exAsync.Count или exAsync.First() во время вызова Add()? Может ли это сломать Add()? Конечно, было бы разумно иметь общий объект блокировки и устанавливать блокировки вокруг доступа к счетчику в цикле ожидания и бите в конце.
Однако, даже если все, что обращается к exAsync, на самом деле не делает этого в основном потоке, вокруг вызова Add () есть блок lock(). Моим первым побуждением было заменить List на System.Collections.Concurrent.ConcurrentBag, но у меня нет особых оснований полагать, что это решит проблему.
Имеет ли это какой-либо смысл для кого-либо?
2 ответа
Просто запираюсь Add
не решит проблему; это просто гарантирует, что два разных Add
звонки не мешают друг другу. Состояние гонки, которое вы определили с завершением цикла ожидания Add
вызов действителен и вызовет проблему, которую вы видите. Вы также должны заблокировать весь блок if/else, который проверяет exAsync
,
Вы не должны просто заменить список ConcurrentBag
как вы могли бы получить другую проблему: чтение из пакета до того, как последнее исключение будет вставлено в список.
(править) Я бы также использовал ManualResetEventSlim для блокировки потока, а не цикла ожидания. Вы можете сделать так, чтобы ваш главный поток ожидал его, а последний рабочий поток сигнализировал об этом, когда счетчик достигнет 0
Также рекомендуется создать закрытый объект и заблокировать его, а не сам список. Таким образом, вы можете четко указать, что вы синхронизируете.
Проблема в том, как используется оператор блокировки. Цитата из этого поста:
Наконец, существует распространенное заблуждение, что lock (this) фактически изменяет объект, переданный в качестве параметра, и каким-то образом делает его доступным только для чтения или недоступным. Это неверно Объект, передаваемый в качестве параметра для блокировки, просто служит ключом. Если блокировка на этом ключе уже удерживается, блокировка не может быть выполнена; в противном случае блокировка разрешена.
"Блокировка" вашего списка не помешает другому коду получить доступ к этому объекту. Это просто говорит о том, что никто другой не может создать блокировку, используя список в качестве ключа. ConcurrentBag должен исправить ваше исключение, но если ваш код исключения выброса будет обработан до того, как ваш последний дескриптор завершит добавление исключения в список, он представит возможность пропустить последнее исключение.