Parallel.ForEach在迭代结束时表现为每个的常规

时间:2011-02-02 19:27:35

标签: c# multithreading c#-4.0 parallel-processing task-parallel-library

当我运行这样的东西时,我遇到了这个问题:

Parallel.ForEach(dataTable.AsEnumerable(), row =>
{
   //do processing
}

假设有500多条记录表示870.一旦Parallel.ForEach完成850,它似乎顺序运行,即一次只运行1次。它非常快速地完成了850次操作,但是当它接近迭代结束时,它变得非常慢并且似乎每次都像常规​​一样。我甚至尝试了2000条记录。

我的代码有问题吗?请提出建议。

以下是我正在使用的代码

抱歉,我刚发布了错误的例子。这是正确的代码:

Task newTask = Task.Factory.StartNew(() =>
{
    Parallel.ForEach(dtResult.AsEnumerable(), dr =>
    {
        string extractQuery = "";
        string downLoadFileFullName = "";
        lock (foreachObject)
        {

            string fileName = extractorConfig.EncodeFileName(dr);
            extractQuery = extractorConfig.GetExtractQuery(dr);
            if (string.IsNullOrEmpty(extractQuery)) throw new Exception("Extract Query not found. Please check the configuration");

            string newDownLoadPath = CommonUtil.GetFormalizedDataPath(sDownLoadPath, uKey.CobDate);
            //create folder if it doesn't exist
            if (!Directory.Exists(newDownLoadPath)) Directory.CreateDirectory(newDownLoadPath);
            downLoadFileFullName = Path.Combine(newDownLoadPath, fileName);
        }
        Interlocked.Increment(ref index);

        ExtractorClass util = new ExtractorClass(SourceDbConnStr);
        util.LoadToFile(extractQuery, downLoadFileFullName);
        Interlocked.Increment(ref uiTimerIndex);
    });
});

3 个答案:

答案 0 :(得分:3)

我的猜测:

这看起来具有很高的潜在IO:

  • 数据库+磁盘
  • 与DB和后面的网络通信
  • 将结果写入磁盘

因此,等待IO需要花费大量时间。我的猜测是,随着更多线程被添加到混合中并且IO正在进一步受到压力,等待只会越来越严重。例如,磁盘只有一组磁头,因此您无法同时写入磁盘。如果有大量线程试图同时写入,性能会下降。

尝试限制您使用的最大线程数:

var options = new ParallelOptions { MaxDegreeOfParallelism = 2 };

Parallel.ForEach(dtResult.AsEnumerable(), options, dr =>
{
    //Do stuff
});

<强>更新

在您的代码编辑之后,我会建议以下内容进行一些更改:

  • 减少最大线程数 - 可以试用。
  • 仅执行一次目录检查和创建。

代码:

private static bool isDirectoryCreated;

//...

var options = new ParallelOptions { MaxDegreeOfParallelism = 2 };

Parallel.ForEach(dtResult.AsEnumerable(), options, dr =>
{
    string fileName, extractQuery, newDownLoadPath;

    lock (foreachObject)
    {
        fileName = extractorConfig.EncodeFileName(dr);

        extractQuery = extractorConfig.GetExtractQuery(dr);

        if (string.IsNullOrEmpty(extractQuery))
            throw new Exception("Extract Query not found. Please check the configuration");

        newDownLoadPath = CommonUtil.GetFormalizedDataPath(sDownLoadPath, uKey.CobDate);

        if (!isDirectoryCreated)
        {
            if (!Directory.Exists(newDownLoadPath))
                Directory.CreateDirectory(newDownLoadPath);

            isDirectoryCreated = true;
        }
    }

    string downLoadFileFullName = Path.Combine(newDownLoadPath, fileName);

    Interlocked.Increment(ref index);

    ExtractorClass util = new ExtractorClass(SourceDbConnStr);
    util.LoadToFile(extractQuery, downLoadFileFullName);

    Interlocked.Increment(ref uiTimerIndex);
});

答案 1 :(得分:2)

如果没有相关代码,很难提供详细信息,但一般来说这是预期的行为。 .NET尝试安排任务,使每个处理器均匀忙碌。

但是,这只能是近似的,并非所有任务都花费相同的时间。最后,一些处理器将完成工作,而另一些则不会,并且重新分配工作成本高昂且并非总是有益。

我不知道有关PLinq使用的负载平衡的详细信息,但最重要的是,这种行为永远无法完全阻止。

答案 2 :(得分:1)

假设您将并行性限制为两个线程。 Parallel.ForEach可能有(至少)两种可能的方式。一种方法是启动两个线程,每个线程都有一半要完成的项目。因此,如果你有850个项目,那么实际上会发生的是线程1被赋予前425个项目而线程2被给予第二个425项目块。现在两个线程都开始工作了。处理的项目顺序如下:[0,425,426,1,2,427,3,428,429,4,...]。

很可能(实际上可能)其中一个线程将比其他线程更快地完成其项目组。

它可以工作的另一种方式是启动两个线程并让每个线程从列表中获取一个项目,处理它,然后获取下一个项目,重复直到没有剩余的项目要处理。在这种情况下,处理的项目顺序更像是[0,1,2,4,3,6,5,...]。

在第一个示例中,每个线程都有一个要处理的项目块。在第二种情况下,每个线程处理来自公共块的项目,直到没有项目为止。

存在各种变化,但这些是在多个线程之间拆分工作的两种主要方式。要么为每个项目提供一组自己的项目,要么期望每个线程在完成一个项目的处理后请求下一个项目。

Parallel.ForEach以第一种方式实现:每个线程都有自己的要处理的项目组。以另一种方式执行此操作将需要更多开销,因为必须将项列表视为共享队列,从而产生同步开销。