【问题标题】:Converting from a ForEach loop to a Parallel.ForEach loop when summarizing into a double slows things down汇总为双精度时从 ForEach 循环转换为 Parallel.ForEach 循环会减慢速度
【发布时间】:2020-12-08 17:40:54
【问题描述】:

我有一段 C# 代码如下。这段代码总结了 DataTable 中的一列“双打”:

var data = this.Db.ExecuteRead(query, this.Score.Name);
var time = 0.0;
foreach (DataRow row in data.Rows)
{
    time += this.ParseDouble(row[0].ToString()) / MillisecondsPerMinute;
}

此代码需要 4 秒才能执行。我想加快速度,所以我将其并行化如下:

Parallel.ForEach(
                data.AsEnumerable(),
                row =>
                    {
                        time += this.ParseDouble(row[0].ToString()) / MillisecondsPerMinute;
                    });

此代码需要 3 秒才能执行。它也会导致碰撞。我不认为“双”线程是安全的。这是意料之中的。然后我添加了一个 Mutex 以使其线程安全:

Parallel.ForEach(
                data.AsEnumerable(),
                row =>
                    {
                        mut.WaitOne();
                        ptime += this.ParseDouble(row[0].ToString()) / MillisecondsPerMinute;
                        mut.ReleaseMutex();
                    });

这段代码要慢得多。执行需要 15 秒,但会产生准确的结果。我的问题是,我最好还是在这里使用标准的“ForEach”,还是可以以更好的方式实现多线程?

作为参考,这里是 ParseDouble 方法:

protected double ParseDouble(string text)
{
    double value;
    if (!double.TryParse(text, out value))
    {
        throw new DoubleExpectedException();
    }

    return value;
}

【问题讨论】:

  • 并非所有问题都可以通过并行/并发解决方案更快地解决。这取决于占用最多时间的内容。首先检查var data = Db.ExecuteRead(query, Score.Name).ToList(); 需要多长时间。如果这很慢,那么优化处理将解决不了多少问题。然后对于这样的事情,最好对问题进行分区,计算数据块的值。然后在你完成后合并所有的块结果(应该快速简单)。有一个 Parallel.ForEach 重载需要 Partitioner 来帮助解决这个问题。
  • 您能否在问题中包含ParseDouble 方法?还有多少行有DataTable
  • “它也会导致冲突。” double time这个变量最后没有正确的值吗?
  • @TheodorZoulias 关于碰撞:当我不使用 Mutex 时,double time 总是不同的,并且总是低于应有的值。
  • @TheodorZoulias 我已将 ParseDouble 方法添加到问题中。

标签: c# thread-safety parallel.foreach


【解决方案1】:

这里有一些方法。首先是一个简单的Parallel.ForEach,将受保护区域(lock)减少到所需的绝对最小值(更新共享状态)。这应该可以最大限度地减少对锁的争用。

DataTable data = this.Db.ExecuteRead(query, this.Score.Name);
double totalTime = 0.0;
Parallel.ForEach(data.AsEnumerable(), row =>
{
    double time = Double.Parse(row[0].ToString()) / MillisecondsPerMinute;
    lock (data) { totalTime += time; }
});

PLINQ 方法。简单且安全,但可能不是最有效的:

double totalTime = data
    .AsEnumerable()
    .AsParallel()
    .Select(row => Double.Parse(row[0].ToString()) / MillisecondsPerMinute)
    .Sum();

Parallel.ForEachPartitioner.Create 的组合应该提供最佳性能,因为它允许将工作负载分块:

double totalTime = 0.0;
Parallel.ForEach(Partitioner.Create(0, data.Rows.Count), () => 0.0D,
    (range, state, accumulator) =>
{
    for (int i = range.Item1; i < range.Item2; i++)
    {
        DataRow row = data.Rows[i];
        accumulator += Double.Parse(row[0].ToString()) / MillisecondsPerMinute;
    }
    return accumulator;
}, accumulator =>
{
    lock (data) { totalTime += accumulator; }
});

【讨论】:

    【解决方案2】:

    这并不总是可取的。对于快速循环体,Parallel.ForEach 会降低性能。 还有一件事是,如果您的迭代项不依赖于您之前的项,那么请继续使用它,但如果它们相互依赖,那么我建议您使用常规 foreach。

    【讨论】:

      猜你喜欢
      • 2019-02-20
      • 2012-01-03
      • 2012-03-03
      • 1970-01-01
      • 2021-03-12
      • 2014-12-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多