【问题标题】:How to do proper Parallel.ForEach, locking and progress reporting如何进行正确的 Parallel.ForEach、锁定和进度报告
【发布时间】:2017-01-23 18:32:43
【问题描述】:

我正在尝试实现Parallel.ForEach 模式并跟踪进度,但我缺少一些关于锁定的信息。以下示例在 threadCount = 1 时计数为 1000,但在 threadCount > 1 时不计数。正确的方法是什么?

class Program
{
   static void Main()
   {
      var progress = new Progress();
      var ids = Enumerable.Range(1, 10000);
      var threadCount = 2;

      Parallel.ForEach(ids, new ParallelOptions { MaxDegreeOfParallelism = threadCount }, id => { progress.CurrentCount++; });

      Console.WriteLine("Threads: {0}, Count: {1}", threadCount, progress.CurrentCount);
      Console.ReadKey();
   }
}

internal class Progress
{
   private Object _lock = new Object();
   private int _currentCount;
   public int CurrentCount
   {
      get
      {
         lock (_lock)
         {
            return _currentCount;
         }
      }
      set
      {
         lock (_lock)
         {
            _currentCount = value;
         }
      }
   }
}

【问题讨论】:

    标签: c# locking task-parallel-library


    【解决方案1】:

    从多个线程(共享count 变量)调用类似count++ 的东西的常见问题是可能会发生以下事件序列:

    1. 线程 A 读取 count 的值。
    2. 线程 B 读取 count 的值。
    3. 线程 A 递增其本地副本。
    4. 线程 B 增加其本地副本。
    5. 线程 A 将增加的值写回count
    6. 线程 B 将增加的值写回count

    这样,线程 A 写入的值被线程 B 覆盖,所以该值实际上只增加了一次。

    您的代码在操作 1、2 (get) 和 5、6 (set) 周围添加了锁,但这并不能阻止有问题的事件序列。

    你需要做的是锁定整个操作,这样当线程 A 递增值时,线程 B 根本无法访问它:

    lock (progressLock)
    {
        progress.CurrentCount++;
    }
    

    如果您知道只需要递增,您可以在 Progress 上创建一个方法来封装它。

    【讨论】:

    • 谢谢,到底发生了什么。我在 Progress 上添加了一个额外的方法来增加应用锁定的计数器。
    • 我认为变量progressLock应该是“progress”?
    【解决方案2】:

    老问题,但我认为有更好的答案。

    您可以使用Interlocked.Increment(ref progress) 报告进度,这样您就不必担心将写入操作锁定为进度。

    【讨论】:

    • 实际上应该是公认的答案先生。在你的代码中添加一些“随机”锁(增加一个计数器似乎是一个很好的例子)每次都必须避免。 Interlocked.Increment 为您提供了这个机会。
    【解决方案3】:

    最简单的解决方案实际上是用字段替换属性,并且

    lock { ++progress.CurrentCount; }
    

    (我个人更喜欢前置增量的外观而不是后置增量,因为“++。”这件事在我的脑海中发生了冲突!但后置增量当然也可以工作。)

    这将具有减少开销和争用的额外好处,因为更新字段比调用更新字段的方法更快。

    当然,将其封装为属性也有好处。 IMO,由于字段和属性语法是相同的,当属性是自动实现或等效时,在字段上使用属性的唯一优势是当您有一个场景时,您可能想要部署一个程序集而不必构建和部署依赖重新组装。否则,您不妨使用更快的字段!如果需要检查值或添加副作用,您只需将字段转换为属性并再次构建。因此,在许多实际情况下,使用字段不会受到任何惩罚。

    但是,我们生活在这样一个时代,许多开发团队都以教条主义的方式运作,并使用 StyleCop 等工具来强化他们的教条主义。这样的工具,不像程序员,不够聪明,无法判断什么时候使用字段是可以接受的,所以“简单到连 StyleCop 都可以检查的规则”总是变成“将字段封装为属性”,“不要使用公共字段”等等……

    【讨论】:

      【解决方案4】:

      从属性中移除锁语句并修改主体:

       object sync = new object();
              Parallel.ForEach(ids, new ParallelOptions {MaxDegreeOfParallelism = threadCount},
                               id =>
                                   {
                                       lock(sync)
                                       progress.CurrentCount++;
                                   });
      

      【讨论】:

      • 我认为循环体中的 lock 语句是原子的。属性中的锁语句是分开的,因为属性是在方法中编译的。
      • 因为增量运算符涉及对 getter 和 setter 的调用 - x++ 是 x = x + 1 的简写 - 所以在读取值之前获取锁,并在更新后释放。但我仍然认为一个领域会更好。 :)
      【解决方案5】:

      这里的问题是++ 不是原子的——一个线程可以在另一个线程读取值和存储(现在不正确的)递增值之间读取和递增值。这可能是因为有一个属性包裹了您的 int

      例如

      Thread 1        Thread 2
      reads 5         .
      .               reads 5
      .               writes 6
      writes 6!       .
      

      setter 和 getter 周围的锁无济于事,因为没有什么可以阻止 lock 阻止它们被乱序调用。

      通常,我建议使用Interlocked.Increment,但您不能将其与属性一起使用。

      相反,您可以公开_lock 并在progress.CurrentCount++; 调用周围设置lock 块。

      【讨论】:

      • 我认为暴露锁是个坏主意,因为这意味着任何人都可以使用它,这很容易导致死锁。我认为更好的解决方案是直接在Main() 中锁定。
      • 如果您在其他地方有代码使用int 执行其他操作,则该代码更容易锁定同一个对象,而不是尝试在所有对象之间共享一个单独的锁定对象。跨度>
      【解决方案6】:

      最好将任何数据库或文件系统操作存储在本地缓冲区变量中,而不是锁定它。锁定会降低性能。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-12-17
        • 2020-07-04
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多