我们有一些代码创建了许多 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,但我没有特别的理由相信这会解决问题。
这对任何人都有意义吗?