Parallel.ForEach不断产生新线程

时间:2012-12-26 10:11:43

标签: c# .net concurrency task-parallel-library parallel-extensions

当我在我的程序中使用Parallel.ForEach时,我发现有些线程似乎永远不会完成。事实上,它一直在反复产生新的线程,这种行为是我没想到的,绝对不想要的。

我能够使用以下代码重现此行为,就像我的“真实”程序一样,它们都使用处理器和内存(.NET 4.0代码):

public class Node
{
    public Node Previous { get; private set; }

    public Node(Node previous)
    {
        Previous = previous;
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        DateTime startMoment = DateTime.Now;
        int concurrentThreads = 0;

        var jobs = Enumerable.Range(0, 2000);
        Parallel.ForEach(jobs, delegate(int jobNr)
        {
            Interlocked.Increment(ref concurrentThreads);

            int heavyness = jobNr % 9;

            //Give the processor and the garbage collector something to do...
            List<Node> nodes = new List<Node>();
            Node current = null;
            for (int y = 0; y < 1024 * 1024 * heavyness; y++)
            {
                current = new Node(current);
                nodes.Add(current);
            }

            TimeSpan elapsed = DateTime.Now - startMoment;
            int threadsRemaining = Interlocked.Decrement(ref concurrentThreads);
            Console.WriteLine("[{0:mm\\:ss}] Job {1,4} complete. {2} threads remaining.", elapsed, jobNr, threadsRemaining);
        });
    }
}

在我的四核上运行时,它最初启动时有4个并发线程,正如您所期望的那样。但是,随着时间的推移,越来越多的线程被创建。最后,该程序然后抛出OutOfMemoryException:

[00:00] Job    0 complete. 3 threads remaining.
[00:01] Job    1 complete. 4 threads remaining.
[00:01] Job    2 complete. 4 threads remaining.
[00:02] Job    3 complete. 4 threads remaining.
[00:05] Job    9 complete. 5 threads remaining.
[00:05] Job    4 complete. 5 threads remaining.
[00:05] Job    5 complete. 5 threads remaining.
[00:05] Job   10 complete. 5 threads remaining.
[00:08] Job   11 complete. 5 threads remaining.
[00:08] Job    6 complete. 5 threads remaining.
...
[00:55] Job   67 complete. 7 threads remaining.
[00:56] Job   81 complete. 8 threads remaining.
...
[01:54] Job  107 complete. 11 threads remaining.
[02:00] Job  121 complete. 12 threads remaining.
..
[02:55] Job  115 complete. 19 threads remaining.
[03:02] Job  166 complete. 21 threads remaining.
...
[03:41] Job  113 complete. 28 threads remaining.
<OutOfMemoryException>

上述实验的内存使用情况图如下:

Processor and memory usage

屏幕截图是荷兰语;顶部代表处理器使用情况,底部部分是内存使用情况。)正如您所看到的,看起来几乎每次垃圾都会产生一个新线程收集器阻碍了(可以在内存使用量的下降中看到)。

任何人都可以解释为什么会这样,我能做些什么呢?我只是希望.NET停止生成新线程,并首先完成现有线程......

3 个答案:

答案 0 :(得分:16)

您可以通过指定ParallelOptions属性集的MaxDegreeOfParallelism实例来限制创建的最大线程数:

var jobs = Enumerable.Range(0, 2000);
ParallelOptions po = new ParallelOptions
{ 
    MaxDegreeOfParallelism = Environment.ProcessorCount
};

Parallel.ForEach(jobs, po, jobNr =>
{
    // ...
});

关于为什么你得到了你正在观察的行为:默认情况下,TPL (它是PLINQ的基础)可以自由地猜出最佳数字要使用的线程数。每当并行任务阻塞时,任务调度器可以创建新线程以维持进度。在你的情况下,阻塞可能会隐含发生;例如,在垃圾收集期间通过Console.WriteLine调用或(如您所见)。

来自Concurrency Levels Tuning with Task Parallel Library (How Many Threads to Use?)

  

由于TPL默认策略是每个处理器使用一个线程,我们可以得出结论,TPL最初假定任务的工作负载大约100%工作且0%等待,并且如果初始假设失败并且任务进入等待状态(即开始阻塞) - TPL可以自由地添加线程。

答案 1 :(得分:6)

您可能应该阅读一下任务调度程序的工作原理。

http://msdn.microsoft.com/en-us/library/ff963549.aspx(页面的后半部分)

  

“.NET线程池自动管理worker的数量   池中的线程。它根据内置添加和删除线程   启发式。 .NET线程池有两种主要的注入机制   线程:一种饥饿避免机制,可以添加工作线程   它看到排队的物品和爬山没有取得任何进展   尝试在使用尽可能少的线程时最大化吞吐量的启发式算法   尽可能。

     

避免饥饿的目标是防止死锁。这种   当工作线程等待同步时,可能会发生死锁   只能由仍处于待处理状态的工作项来满足的事件   在线程池的全局或本地队列中。如果有固定的话   工作线程数,所有这些线程都是类似的   被阻止,系统将无法取得进一步的进展。   添加新的工作线程可以解决问题。

     

爬山启发式的目标是提高其利用率   线程被I / O或其他等待条件阻塞的核心   停止处理器。默认情况下,托管线程池有一个   每个核心的工作线程。如果其中一个工作线程成为   被阻止,核心可能未被充分利用,   取决于计算机的总体工作量。线程注入   逻辑不区分被阻塞的线程和线程   这是一项冗长的处理器密集型操作。因此,   每当线程池的全局或本地队列包含待处理的工作时   项目,需要很长时间才能运行的活动工作项目(超过一个   半秒)可以触发创建新的线程池工作者   线程“。

您可以将任务标记为 LongRunning ,但这会产生从线程池外部为其分配线程的副作用,这意味着该任务无法内联。

请记住, ParallelFor 将作为块给出的工作视为块,因此即使一个循环中的工作相当小,由外观调用的任务所完成的整体工作对于调度程序来说可能看起来更长

大多数对GC及其自身的调用都没有阻塞(它在一个单独的线程上运行)但是如果等待GC完成则会阻塞。还要记住,GC正在重新安排内存,因此如果您在运行GC时尝试分配内存,则可能会产生一些副作用(和阻塞)。我没有具体细节,但我知道PPL有一些专门用于并发内存管理的内存分配功能。

查看代码的输出,似乎事情正在运行很多秒。所以我看到线程注入并不奇怪。但是我似乎记得默认的线程池大小大约是30个线程(可能取决于系统上的核心数)。在你的代码分配之前,一个线程占用了大约一MB的内存,所以我不清楚你为什么会在这里得到一个内存不足的例外。

答案 2 :(得分:1)

我发布了后续问题"How to count the amount of concurrent threads in .NET application?"

如果要直接计算线程,那么它们在Parallel.For()中的数量大多数((非常少且不显着地减少)仅增加并且在循环完成后不会释放。

在发布和调试模式下使用

进行检查
ParallelOptions po = new ParallelOptions
{
  MaxDegreeOfParallelism = Environment.ProcessorCount
};

且没有

数字各不相同,但结论相同。

以下是我正在使用的准备好的代码,如果有人想要玩:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Edit4Posting
{
public class Node
{

  public Node Previous { get; private set; }
  public Node(Node previous)
  {
    Previous = previous;
    }
  }
  public class Edit4Posting
  {

    public static void Main(string[] args)
    {
      int concurrentThreads = 0;
      int directThreadsCount = 0;
      int diagThreadCount = 0;

      var jobs = Enumerable.Range(0, 160);
      ParallelOptions po = new ParallelOptions
      {
        MaxDegreeOfParallelism = Environment.ProcessorCount
      };
      Parallel.ForEach(jobs, po, delegate(int jobNr)
      //Parallel.ForEach(jobs, delegate(int jobNr)
      {
        int threadsRemaining = Interlocked.Increment(ref concurrentThreads);

        int heavyness = jobNr % 9;

        //Give the processor and the garbage collector something to do...
        List<Node> nodes = new List<Node>();
        Node current = null;
        //for (int y = 0; y < 1024 * 1024 * heavyness; y++)
        for (int y = 0; y < 1024 * 24 * heavyness; y++)
        {
          current = new Node(current);
          nodes.Add(current);
        }
        //*******************************
        directThreadsCount = Process.GetCurrentProcess().Threads.Count;
        //*******************************
        threadsRemaining = Interlocked.Decrement(ref concurrentThreads);
        Console.WriteLine("[Job {0} complete. {1} threads remaining but directThreadsCount == {2}",
          jobNr, threadsRemaining, directThreadsCount);
      });
      Console.WriteLine("FINISHED");
      Console.ReadLine();
    }
  }
}