为什么Parallel.Foreach会创建无穷无尽的线程?

时间:2012-06-22 00:07:51

标签: c# .net multithreading task-parallel-library

下面的代码继续创建线程,即使队列是空的。最终会发生OutOfMemory异常。如果我用常规foreach替换Parallel.ForEach,则不会发生这种情况。有人知道为什么会这样吗?

public delegate void DataChangedDelegate(DataItem obj);

public class Consumer
{
    public DataChangedDelegate OnCustomerChanged;
    public DataChangedDelegate OnOrdersChanged;

    private CancellationTokenSource cts;
    private CancellationToken ct;
    private BlockingCollection<DataItem> queue;

    public Consumer(BlockingCollection<DataItem> queue) {
        this.queue = queue;
        Start();
    }

    private void Start() {
        cts = new CancellationTokenSource();
        ct = cts.Token;
        Task.Factory.StartNew(() => DoWork(), ct);
    }

    private void DoWork() {

        Parallel.ForEach(queue.GetConsumingPartitioner(), item => {
            if (item.DataType == DataTypes.Customer) {
                OnCustomerChanged(item);
            } else if(item.DataType == DataTypes.Order) {
                OnOrdersChanged(item);
            }
        });
    }
}

3 个答案:

答案 0 :(得分:5)

我认为Parallel.ForEach()主要用于处理有界集合。并且它不会期望像GetConsumingPartitioner()返回的集合,其中MoveNext()会长时间阻塞。

问题在于Parallel.ForEach()尝试找到最佳并行度,因此它会在Task允许运行时启动尽可能多的TaskScheduler。但是TaskScheduler看到有很多Task需要很长时间才能完成,并且他们没有做任何事情(他们阻止)所以它继续开始新的。{/ p>

我认为最好的解决方案是设置MaxDegreeOfParallelism

作为替代方案,您可以使用TPL Dataflow的ActionBlock。这种情况的主要区别在于ActionBlock在没有要处理的项时不会阻塞任何线程,因此线程数不会接近限制。

答案 1 :(得分:3)

生产者/消费者模式主要在只有一个生产者和一个消费者时使用。

但是,您尝试实现的目标(多个消费者)更适合工作清单模式。下面的代码来自于犹他大学教授的并行编程课程中的unit2幻灯片“2c - 共享内存模式”的幻灯片,可在http://ppcp.codeplex.com/下载

BlockingCollection<Item> workList;
CancellationTokenSource cts;
int itemcount

public void Run()
{
  int num_workers = 4;

  //create worklist, filled with initial work
  worklist = new BlockingCollection<Item>(
    new ConcurrentQueue<Item>(GetInitialWork()));

  cts = new CancellationTokenSource();
  itemcount = worklist.Count();

  for( int i = 0; i < num_workers; i++)
    Task.Factory.StartNew( RunWorker );
}

IEnumberable<Item> GetInitialWork() { ... }

public void RunWorker() {
  try  {
    do {
      Item i = worklist.Take( cts.Token );
      //blocks until item available or cancelled
          Process(i);
      //exit loop if no more items left
    } while (Interlocked.Decrement( ref itemcount) > 0);
  } finally {
      if( ! cts.IsCancellationRequested )
        cts.Cancel();
    }
  }
}

public void AddWork( Item item) {
  Interlocked.Increment( ref itemcount );
  worklist.Add(item);
}

public void Process( Item i ) 
{
  //Do what you want to the work item here.
}

上述代码允许您将工作列表项添加到队列中,并允许您设置任意数量的工作程序(在本例中为4)以将项目拉出队列并进行处理。

.Net 4.0上的Parallelism的另一个重要资源是“与Microsoft .Net并行编程”一书,可在以下网址免费获取:http://msdn.microsoft.com/en-us/library/ff963553

答案 2 :(得分:1)

在任务并行库的内部,Parallel.For和Parallel.Foreach遵循爬山算法来确定应该使用多少并行度。

或多或少,他们开始在一个任务上运行正文,移动到两个,依此类推,直到达到一个断点并且他们需要减少任务数量。

这对于快速完成的方法体非常有效,但如果身体需要很长时间才能运行,它可能需要很长时间才能实现它需要减少并行度。在此之前,它会继续添加任务,并可能导致计算机崩溃。

我在任务并行库的一位开发人员的演讲中学到了上述内容。

指定MaxDegreeOfParallelism可能是最简单的方法。