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 должен исправить ваше исключение, но если ваш код исключения выброса будет обработан до того, как ваш последний дескриптор завершит добавление исключения в список, он представит возможность пропустить последнее исключение.

Другие вопросы по тегам