C# Parallel.ForEach不断生成新线程

C# Parallel.ForEach不断生成新线程,c#,.net,concurrency,task-parallel-library,parallel-extensions,C#,.net,Concurrency,Task Parallel Library,Parallel Extensions,当我在程序中使用Parallel.ForEach时,我发现有些线程似乎从未完成。事实上,它一次又一次地产生新的线程,这种行为我没有预料到,也绝对不想要 我能够用以下代码重现这种行为,就像我的“真实”程序一样,大量使用处理器和内存(.NET 4.0代码): 公共类节点 { 公共节点前一个{get;private set;} 公共节点(上一个节点) { 先前=先前; } } 公共课程 { 公共静态void Main(字符串[]args) { DateTime startMoment=DateTime

当我在程序中使用Parallel.ForEach时,我发现有些线程似乎从未完成。事实上,它一次又一次地产生新的线程,这种行为我没有预料到,也绝对不想要

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

公共类节点
{
公共节点前一个{get;private set;}
公共节点(上一个节点)
{
先前=先前;
}
}
公共课程
{
公共静态void Main(字符串[]args)
{
DateTime startMoment=DateTime.Now;
int concurrenthreads=0;
变量作业=可枚举范围(0,2000);
Parallel.ForEach(作业、委托(int-jobNr)
{
联锁增量(参考并发线程);
整数权重=作业编号%9;
//给处理器和垃圾收集器一些事情做。。。
列表节点=新列表();
节点电流=零;
对于(整数y=0;y<1024*1024*重;y++)
{
当前=新节点(当前);
节点。添加(当前);
}
TimeSpan Appeased=DateTime.Now-startMoment;
int THREADSREAIN=联锁。减量(参考concurrentThreads);
WriteLine(“[{0:mm\\\:ss}]作业{1,4}完成。{2}个线程剩余。”,已用,作业编号,线程保留);
});
}
}
当在我的四核上运行时,它最初以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>
[00:00]作业0已完成。剩下3个线程。
[00:01]作业1已完成。剩余4个线程。
[00:01]作业2已完成。剩余4个线程。
[00:02]作业3已完成。剩余4个线程。
[00:05]作业9已完成。剩余5个线程。
[00:05]作业4已完成。剩余5个线程。
[00:05]作业5已完成。剩余5个线程。
[00:05]作业10已完成。剩余5个线程。
[00:08]作业11已完成。剩余5个线程。
[00:08]作业6已完成。剩余5个线程。
...
[00:55]作业67已完成。剩余7个线程。
[00:56]作业81已完成。剩余8个线程。
...
[01:54]作业107已完成。剩下11条线。
[02:00]作业121已完成。剩下12条线。
..
[02:55]作业115已完成。剩下19条线。
[03:02]作业已完成。剩下21条线。
...
[03:41]作业113已完成。剩下28条线。
上述实验的内存使用图如下所示:

(屏幕截图是荷兰语;顶部表示处理器使用情况,底部表示内存使用情况。)如您所见,垃圾收集器几乎每次遇到阻碍时都会产生一个新线程(如内存使用率下降所示)


有人能解释为什么会发生这种情况,我能做些什么吗?我只希望.NET停止生成新线程,并首先完成现有线程…

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

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

Parallel.ForEach(jobs, po, jobNr =>
{
    // ...
});
至于为什么会出现您正在观察的行为:默认情况下,TPL(PLINQ的基础)可以自由猜测要使用的最佳线程数。每当并行任务阻塞时,任务调度器可能会创建一个新线程以保持进度。在您的情况下,阻塞可能是隐式发生的;例如,通过
Console.WriteLine
调用,或者(正如您所观察到的)在垃圾收集期间

发件人:

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


您可能应该阅读一些关于任务调度器如何工作的信息

(下半页)

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

避免饥饿的目标是防止僵局 工作线程等待同步时可能发生死锁 事件,该事件只能由仍挂起的工作项满足 在线程池的全局或本地队列中。如果存在固定的 工作线程的数量,并且所有这些线程都是相似的 如果被阻止,系统将无法取得进一步的进展。 添加新的工作线程可以解决此问题

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

您可以将任务标记为长时间运行,但这是不正确的
var jobs = Enumerable.Range(0, 2000);
ParallelOptions po = new ParallelOptions
{ 
    MaxDegreeOfParallelism = Environment.ProcessorCount
};

Parallel.ForEach(jobs, po, jobNr =>
{
    // ...
});
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();
    }
  }
}