List.Add中的BackgroundWorker.RunWorkerCompleted异常

时间:2013-11-20 15:22:19

标签: .net multithreading backgroundworker list-template

我们有一些代码可以创建许多BackgroundWorker线程,每个线程都有一些数据库内容。有时这些线程会抛出一个异常(通常是由于超时 - 这是最近的事情,而且我不是那个必须解决这个问题的人)。

如果任何线程失败,整个操作都是无用的,整个过程都发生在Web服务调用中。因此,在失败时,我们需要在主线程中抛出异常,该异常将被捕获并转换为客户端的SOAP错误异常。

我们在List中收集线程异常。在几十次这个代码的运行中,多达七个工作线程都在同一时间抛出异常,有一次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(normally it isn't,但这是Web服务,看起来像behavior outside a Windows application will differ)。

异常看起来像一样,List.Add由线程1调用,然后在第一次调用仍在执行且对象仍处于不一致状态时由线程2调用。由于多个线程达到默认的30秒SqlCommand超时,因此总是多次失败(实际上到目前为止),它们将在同一时间内完成。我可以在一个小测试应用程序中重新创建该行为,如果列表中没有锁定

可能是因为它恰好在Add之前递增ctThreadsFinished以通过等待循环,所以它在Add()调用期间访问exAsync.Count或exAsync.First()吗?可以打破Add()吗?拥有共享锁对象并在等待循环中的计数器访问周围放置锁定以及结束时的位当然是明智的。

但是,即使访问exAsync的所有内容实际上都没有在主线程中这样做,Add()调用周围也会有一个lock()块。我的第一个冲动是用System.Collections.Concurrent.ConcurrentBag替换List,但我没有特别的理由相信这将解决问题。

这对任何人都有意义吗?

2 个答案:

答案 0 :(得分:1)

仅锁定Add无法解决问题;这只是确保两个不同的Add调用不会相互干扰。您在调用Add之前完成等待循环时识别出的竞争条件是有效的,并且会导致您看到的问题。您还应该锁定正在检查exAsync的整个if / else块。

您不应该仅使用ConcurrentBag替换列表,因为您可能会遇到另一个问题:在将最后一个异常插入列表之前从包中读取。

(编辑)我还会使用ManualResetEventSlim来阻止线程而不是睡眠循环。您可以让主线程等待它,最后一个工作线程在计数到0时发出信号。

同样最好创建一个私有对象并锁定它,而不是列表本身。这样你就可以明确你正在同步的内容。

答案 1 :(得分:1)

问题在于使用lock语句的方式。来自this帖子的引用:

  

最后,有一种常见的误解,即lock(this)实际上修改了作为参数传递的对象,并以某种方式使其成为只读或不可访问。这是错误的。作为锁定参数传递的对象仅用作键。如果已经锁定了该锁,则无法进行锁定;否则,允许锁定。

“锁定”您的列表不会阻止其他代码访问该对象。它只是说没有其他人可以使用列表作为密钥来创建锁。 ConcurrentBag应该修复你的异常,但是如果你的抛出异常代码在最后一个句柄完成之前被命中,那么它会引入你错过最后一个异常的可能性。