【问题标题】:Understanding Parallel.For Loop and local variables了解 Parallel.For 循环和局部变量
【发布时间】:2020-03-30 21:49:16
【问题描述】:

我是并行计算的新手,在 C# 中运行 Parallel.For 时遇到了一些问题。 我正在尝试同时访问多个网站,获取 HTML 并将它们注册到多个 SQLite 数据库中。 在我更准确地检查结果之前,一切似乎都很好。 我注意到在 0 到 20 的一个循环中,代码在循环的共享部分输入了 20 次,而在本地部分只输入了 16 次。因此,缺少 4 个结果。 为了理解这个问题,我做了一个我只放两个计数器的经验。一个在全局部分,另一个在本地。全局计数的输出为 20,本地部分为 1!之后,我在全局部分返回本地部分之前睡了 2 秒。在这种情况下,全局计数的输出为 20,本地部分为 13!你能解释一下我做错了什么吗?

static void ParalellCalc()
        {
            var tm = new Stopwatch();
            tm.Start();
            int count = 0;
            int count2 = 0;
            var parl = Parallel.For<int>(0, 20, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount}, () => 0, (i, state, Enrada) =>
            {
                count++;
                Thread.Sleep(2000);
                return Enrada;
            },
            (x) =>
            {
                count2++;
            }
            );

            tm.Stop();
            Console.WriteLine(tm.Elapsed);
            Console.WriteLine("Global: " + count.ToString());
            Console.WriteLine("Local: " + count2.ToString());
            Console.WriteLine(tm.Elapsed);
            tm.Reset();
        }

编辑: 我考虑了你的建议,我用 Interlocked.Increment 做了同样的例子来增加计数器。产生的结果完全相同。如果我删除 Thread.Sleep(2000) 第二个计数器产生 1 的结果!?如果我不删除产生 16 的结果。在所有情况下,第一个计数器显示的值应该是 20。谁能解释一下?

static void ParalellCalc()
        {
            var tm = new Stopwatch();
            tm.Start();
            int count = 0;
            int count2 = 0;
            var parl = Parallel.For<int>(0, 20, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount}, () => 0, (i, state, Enrada) =>
            {
                Interlocked.Increment(ref count);
                return Enrada;
            },
            (x) =>
            {
                Interlocked.Increment(ref count2);
            });

            tm.Stop();
            Console.WriteLine(tm.Elapsed);
            Console.WriteLine("Global: " + count.ToString());
            Console.WriteLine("Local: " + count2.ToString());
            Console.WriteLine(tm.Elapsed);
            tm.Reset();
        } 

【问题讨论】:

  • 您确定要并行而不是异步执行此操作吗?下载数据是一项 I/O 绑定操作,不太可能从并行性中受益匪浅。
  • Parrellel 不管理线程,因此每次睡眠可能为 2 秒,但也可能更长,具体取决于池的管理方式。基本上它会创建一个线程,然后将其交给一个池来管理工作何时完成。仅仅因为您首先创建了一个线程并不意味着经理将首先甚至下一个开始处理该线程。
  • Parallel 类是启动 I/O 操作的错误工具。当您需要为 您的 CPU 工作而不是为远程 Web 服务器的 CPU 工作时,请使用此类。适合这项工作的工具是TPL Dataflow Library。该库提供了全套选项,可以处理 CPU 密集型和 I/O 密集型操作,并允许您精确控制并发级别。
  • @DanielMann 我从来没有以异步的方式思考过。你有什么建议?下载 url 时异步获取 HTML 并触发将数据保存在数据库中的事件?
  • @TheodorZoulias 这里的问题不是 I/O 进程的管理。我认为我的问题不在于。我写数据没有问题。我的问题是每个线程都没有返回到本地函数。做了这个例子,证明并非所有线程都经过循环的本地部分。你能解释一下为什么没有 Thread.Sleep(2000) 循环只能在本地部分准时进入吗?最好的问候。

标签: c# loops for-loop parallel-processing parallel.foreach


【解决方案1】:

++ 运算符不是线程安全的,因为它不是原子的。 Interlocked.Increment 是线程安全的。 Interlocked.Increment(ref count) 而不是 count++count2 相同可能会解决计数问题。

【讨论】:

  • 是的,你说得对。循环不是线程安全的,我现在就是这样。但我的问题是。独立于代码是否线程安全,在进程的最后我不应该有count = 20和count2 = 20?循环不应该每个线程一次进入本地部分吗?
【解决方案2】:

Parallel.For 并行执行您传入的操作(不保证,但在这种情况下很可能是真的)。因此,首先要注意的是,您的 parallel.for 内部存在竞争条件,可能会从多个线程访问和写入计数器。通过使用锁定机制(例如lock(obj))围绕代码的counter++ 部分,您应该能够解决竞争条件。

【讨论】:

  • 您最好使用@Renat 的Interlocked 解决方案,而不是使用锁定counter++ 语句。
  • @Flydog57 虽然是真的,但我认为 OP 以他发布的代码为例(他提到 SQL-Databases 作为他的实际用例)。因此,我认为在这种情况下提供更通用的锁方法更有用。
  • 但我不明白@EikeS。每个线程都不应该进入本地部分?当循环进入本地部分时,我从全局部分返回的数据没有在这个上下文中锁定?
【解决方案3】:

Parallel.For 方法通过将工作负载拆分为多个分区来并行化工作负载。分区的数量和每个分区的大小由内部启发式确定。您的实验表明,20 个项目的工作负载可以仅拆分为 1 个分区或 16 个分区,具体取决于每个项目的处理持续时间。通过添加Thread.Sleep(2000) 行,您将工作负载从极轻变为相当繁重,因此创建了更多分区来平衡工作负载。这 16 个分区通常由少于 16 个线程处理,因为每个线程处理多个分区。

为了更好地了解Parallel.For 的工作原理,您应该记录比countcount2 这两个计数器更多的信息。您还应该为每个线程设置一个计数器,使用 ConcurrentDictionary&lt;int, int&gt; 和每个线程的 ID 键。

【讨论】:

  • 很好的解释西奥多,我想我现在明白了。您是否建议一些技术来确保每个线程都将在循环的本地部分单独处理?
  • @MarcoTeixeira 是的。您可以创建和使用自己的Partitioner,或者,如果您想要绝对控制,请不要使用Parallel.For!您可以手动启动线程,也可以使用带有Task.Run 的线程池线程。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-22
相关资源
最近更新 更多