【问题标题】:Parallel.For freezes after around 1370 iterations, no idea whyParallel.For 在大约 1370 次迭代后冻结,不知道为什么
【发布时间】:2026-02-13 12:45:02
【问题描述】:

我正在对超过 7500 个对象运行 Parallel.For 循环。在那个 for 循环中,我对每个对象做了很多事情,特别是调用两个 Web 服务和两个内部方法。 Web 服务只是检查对象、处理并返回一个字符串,然后我将其设置为对象的属性。两种内部方法也是如此。

我没有将任何内容写入磁盘或从磁盘读取。

我还在 winforms 应用程序中使用标签和进度条更新 UI,让用户知道它在哪里。代码如下:

var task = Task.Factory.StartNew(() =>
{
  Parallel.For(0, upperLimit, (i, loopState) =>
  {
     if (cancellationToken.IsCancellationRequested)
        loopState.Stop();
     lblProgressBar.Invoke(
       (Action)
       (() => lblProgressBar.Text = string.Format("Processing record {0} of {1}.", (progressCounter++), upperLimit)));
     progByStep.Invoke(
       (Action)
       (() => progByStep.Value = (progressCounter - 1)));

      CallSvc1(entity[i]);
      Conversion1(entity[i]);
      CallSvc2(entity[i]);
      Conversion2(entity[i]);
  });
}, cancellationToken);

这是在 Win7 32 位机器上进行的。

关于为什么当增量器在 1370 左右时突然冻结的任何想法(它一直是 1361、1365 和 1371)?

关于如何调试它并查看锁定的内容(如果有的话)有什么想法吗?

编辑:
下面对 cme​​ts 的一些答案:
@BrokenGlass - 不,没有互操作性。我会尝试 x86 编译并告诉你。

@chibacity - 因为它在后台任务中,所以它不会冻结 UI。直到它冻结时,进度条和标签以大约每秒 2 次的速度滴答作响。当它冻结时,它就停止移动。我可以验证它停止的号码已被处理,但没有更多。双核 2.2GHz 上的 CPU 使用率在运行期间最低,为 3-4%,一旦冻结则为 1-2%。

@Henk Holterman - 到达 1360 大约需要 10 到 12 分钟,是的,我可以验证所有这些记录都已处理,但其余记录未处理。

@CodeInChaos - 谢谢,我会试试的!如果我取出并行代码,代码确实有效,它只需要永远和一天。我没试过限制线程数,但是会的。

编辑 2:
有关 web 服务发生了什么的一些细节

Web 服务的基本情况是它们传递一些数据并接收数据(一个 XmlNode)。然后在 Conversion1 过程中使用该节点,该过程又在实体上设置另一个属性,该属性被发送到 CallSvc2 方法,依此类推。它看起来像这样:

private void CallSvc1(Entity entity)
{
    var svc = new MyWebService();
    var node = svc.CallMethod(entity.SomeProperty);
    entity.FieldToUpdate1.LoadXml(node.InnerXml);
}
private void Conversion1(Entity entity)
{
    // Do some xml inspection/conversion stuff
    if (entity.FieldToUpdate1.SelectSingleNode("SomeNode") == "something") {
        entity.FieldToUpdate2 = SomethingThatWasConverted;
    }
    else {
        // Do some more logic
    }
}
private void CallSvc2(Entity entity)
{
    var svc = new SomeOtherWebService();
    var xmlNode = svc.MethodToCall(entity.FieldToUpdate2.InnerXml);
    entity.AnotherXmlDocument.LoadXml(xmlNode.InnerXml);
}

如您所见,这是非常简单的事情。在一些转换方法中发生了很多事情,但没有一个应该是阻塞的。如下所述,有 1024 个线程处于“等待”状态,它们都在进行 Web 服务调用。我在这里读到http://www.albahari.com/threading/ 32 位机器上的 .Net 4 的 MaxThreads 默认为 1023。

考虑到我在这里的情况,如何释放那些等待的线程?

【问题讨论】:

  • 我之前也遇到过类似的问题 - 我会尝试在 x86 模式下构建项目,看看是否有任何改变。你不会碰巧在你的任务中做任何 InterOp?
  • 它是冻结还是只是非常缓慢。 CPU 使用率如何?
  • 还没有使用过 TPL,但是你不能在调试器中中断并检查函数停止的方法调用吗?如果用普通的 for 循环替换它,代码是否有效?如果您使用 Parallel.For 但将其限制为一两个线程会怎样?
  • 您应该使您的 Web 服务调用异步。 BeginGetResponse 而不是 GetResponse。这会将线程释放回池中。
  • 鉴于您所说的,我认为我们需要了解 CallSvc 方法的内容。您是否有机会分享您如何调用服务的示例?是WCF、WebClient、HttpRequest……?

标签: c# parallel-processing freeze task-parallel-library


【解决方案1】:

一种可能的解释:您已使进程处于无法创建更多线程的状态,这阻碍了工作取得进展,这就是为什么一切都停止了。

坦率地说,无论该假设是否正确,您都需要采取完全不同的方法来解决这个问题。 Parallel.For 是解决此问题的错误方法。 (Parallel 最适合 CPU 密集型工作。这里的工作是 IO 密集型工作。)如果您确实需要处理数千个 Web 服务请求,则需要转而使用异步代码,而不是多线程代码。如果您使用异步 API,您将能够同时启动数千个请求,同时只使用少数线程。

这些请求是否真的能够同时执行是另一回事 - 无论您使用当前的“线程启示录”实现还是更高效的异步实现,您都可能会遇到限制。 (.NET 有时会限制它实际发出的请求数量。)因此,您可以要求发出任意数量的请求,但您可能会发现几乎所有请求都在等待较早的请求完成。例如。我认为WebRequest 将与任何单个域的并发连接限制为仅 2...启动 1000 多个线程(或 1000 多个异步请求)只会导致加载更多请求等待成为 2 个当前请求之一!

您应该自己进行节流。您需要决定同时有多少未完成的请求,并确保一次只启动那么多请求。只是要求Parallel 尽可能快地启动尽可能多的项目,就会让一切陷入困境。

更新添加:

一个快速的解决方法可能是使用接受ParallelOptions 对象的Parallel.For 的重载——您可以设置它的MaxDegreeOfParallelism 属性来限制并发请求的数量。这将阻止这个线程繁重的实现实际上用尽线程。但它仍然是解决问题的低效解决方案。 (据我所知,您确实需要发出数千个并发请求。例如,如果您正在编写一个网络爬虫,这实际上是一件合理的事情。Parallel 不适合那个工作。使用异步操作。如果您使用的 Web 服务代理支持 APM(BeginXxx,EndXxx),您可以将其包装在 Task 对象中 - Task.TaskFactory 提供了一个 FromAsync,它将提供一个任务表示正在进行的异步操作。

但是,如果您要尝试同时处理数千个请求,您确实需要仔细考虑您的限制策略。尽可能快地抛出请求不太可能是最佳策略。

【讨论】:

  • 如果我尝试使用的 Web 服务不支持 APM(BeginXXX 和 EndXXX)但有 SvcNameCompleted 事件处理程序和 SvcNameAsync,我还能使用 TaskFactory FromAsync 吗?
  • 不,TaskFactory.FromAsync 是专门为 APM 设计的。但是,如果您查看msdn.microsoft.com/library/dd997423,“将复杂的 EAP 操作公开为任务”部分确实显示了如何处理 XxxAsync/XxxCompleted 模式。 (EAP,因为该模式是已知的。)
  • 我主要同意更新部分,线程数应该是有限的。对于一个较低的数字,N
  • 少量线程确实可以,但前提是它确实可以解决手头的问题。这取决于应用程序真正需要做什么。对于某些任务(例如网络爬虫),一次激活数千个请求是完全合理的(即使任何单个网站一次只有 2 或 3 个)。所以我们真的需要知道:克里斯康威想要做什么?为什么他要打上千个电话? (从代码中,我怀疑最终的答案将是对他正在使用的 Web 服务进行根本性的重组......批量 FTW!)
【解决方案2】:

在 VS 调试器中运行应用程序。当它似乎被锁定时,告诉 VS 调试:Break All。然后转到 Debug: Windows: Threads 并查看进程中的线程。其中一些应该显示在您的并行 for 循环中的堆栈跟踪,这将告诉您当调试器停止进程时它们在做什么。

【讨论】:

  • 调试线程窗口中有超过 1000 个线程,他们都说线程的位置在两个 Web 服务调用之一上。它们都在类别列中显示黄色和 WorkerThread,并显示“处于睡眠、等待或加入中”。这可能是问题,等待线程的绝对数量吗?如果是这样,我该怎么办?我没有看到任何崩溃或抛出异常。
  • 这听起来很严肃。尝试从并行循环回拨一点。确保它作为一个直接的顺序循环工作,然后尝试限制并行循环中的线程数。
  • 查看 Parallel Tasks 窗口显示它正好有 1024 个等待线程,所有线程都在 Web 服务调用上。不知道为什么它不能将这些线程释放回池中。非 Web 服务调用线程返回到池中。嗯嗯嗯
  • @Chris:1000 个线程还是 1000 个任务?有很大的不同。
  • TPL 任务窗口显示类别是工作线程,其中有 1023 个。 1 主线程也在那里,那就是 program.main。