【问题标题】:BackgroundWorker.RunWorkerCompleted exception in List.AddList.Add 中的 BackgroundWorker.RunWorkerCompleted 异常
【发布时间】:2013-11-20 15:22:18
【问题描述】:

我们有一些代码创建了许多 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(normally it isn't,但这是 Web 服务,看起来像 behavior outside a Windows application will differ)。

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

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

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

这对任何人都有意义吗?

【问题讨论】:

    标签: .net multithreading backgroundworker list-template


    【解决方案1】:

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

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

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

    另外,最好创建一个私有对象并锁定它,而不是列表本身。这样您就可以清楚地知道您正在同步的内容。

    【讨论】:

      【解决方案2】:

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

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

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

      【讨论】:

      • 对,我锁定的对象是任意的。可能是列表,可能是我母亲的 Unicode 娘家姓。它锁定了一段代码,而不是列表本身:msdn.microsoft.com/en-us/library/c5kehkcz(v=vs.110).aspx 我希望它能够工作,因为 exAsync.Add() 仅在该代码段中被调用,而不是因为我使用 exAsync 作为锁定令牌。
      • 是的,但是稍后您访问列表以检查异常的代码不在锁内,因此即使在另一个线程添加最后一个异常时,它也会愉快地访问您的列表。跨度>
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-06
      • 1970-01-01
      • 1970-01-01
      • 2018-02-06
      相关资源
      最近更新 更多