0

我们有一些代码创建了许多 BackgroundWorker 线程,每个线程都做一些数据库工作。有时这些线程会抛出异常(通常是由于超时——这是最近发生的事情,我不是必须解决这个问题的人)。

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

我们在一个列表中收集线程异常。在此代码的数十次中,多达 7 个工作线程几乎同时抛出异常,有一次 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(通常不是,但这是 Web 服务,看起来Windows 应用程序之外的行为会有所不同)。

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

可能是它在 Add() 调用期间在适当的时刻在 Add 之前递增 ctThreadsFinished 以通过等待循环,因此它在 Add() 调用期间访问 exAsync.Count 或 exAsync.First() ?这会破坏 Add() 吗?拥有一个共享锁对象并在等待循环中的计数器访问周围加上锁当然是明智之举,最后的位。

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

这对任何人都有意义吗?

4

2 回答 2

1

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

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

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

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

于 2013-11-20T16:49:33.797 回答
1

问题在于 lock 语句的使用方式。这篇文章的引述:

最后,有一个普遍的误解,认为 lock(this) 实际上修改了作为参数传递的对象,并以某种方式使其只读或不可访问。这是错误的。作为参数传递给 lock 的对象仅用作键。如果该钥匙上已被锁住,则无法上锁;否则,允许锁定。

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

于 2013-11-20T16:53:15.923 回答