【问题标题】:Why my code does not speed up with a multithreaded Parallel.For loop?为什么我的代码没有通过多线程 Parallel.For 循环加速?
【发布时间】:2023-03-07 08:00:01
【问题描述】:

我尝试使用System.Threading.Tasks 库将一个简单的顺序循环转换为并行计算循环。 代码编译,返回正确结果,但不节省任何计算成本,否则耗时较长。


编辑:对不起,伙计们,我可能过于简化了这个问题,并在这样做时犯了一些错误。 为了附加更多信息,我在 i7-4700QM 上运行代码,它在 Grasshopper 脚本中被引用。 这是实际的代码。我也切换到了非线程局部变量

public static class LineNet
{        
    public static List<Ray> SolveCpu(List<Speaker> sources, List<Receiver> targets, List<Panel> surfaces)
    {
        ConcurrentBag<Ray> rays = new ConcurrentBag<Ray>();
        for (int i = 0; i < sources.Count; i++)
        {
            Parallel.For(
                0,
                targets.Count,
                j =>
                {
                    Line path = new Line(sources[i].Position, targets[j].Position);
                    Ray ray = new Ray(path, i, j);
                    if (Utils.CheckObstacles(ray,surfaces))
                    {
                        rays.Add(ray);
                    }

                }
                );
        }
    }
}

Grasshopper 实现只是收集sourcestargetssurfaces,调用方法Solve 并返回rays。 我知道将工作负载分派到线程是昂贵的,但它是如此昂贵吗? 还是ConcurrentBag 只是阻止并行计算?

另外,我的类是不可变的 (?),但如果我使用常见的 List,内核会中止操作并引发异常,有人能说出原因吗?

【问题讨论】:

  • 并行循环通常只会让你的方法运行得更快,如果让你的方法变慢的是处理器(即 - 在四核计算机中,你的 CPU 使用率在整个方法运行期间保持在 25%运行,因为处理器一直在努力工作。)如果这是真的,那么将它分散到多个线程上会有所帮助。如果您的方法在硬盘驱动器或网络资源或其他东西上等待,它可能没有帮助。如果不了解有关如何生成 Lines 或程序正在做什么的更多详细信息,很难说为什么这很慢...
  • 我们在您的测试中讨论了多少行?会有一些初始开销,因此好处可能不会立即明显。另一件事是result.Add(line); 将阻止任何线程也想调用与您使用ConcurrentBag 相同的行
  • This answer 可能提供信息。
  • @ElementalPete 我更新了问题以便更好地解释问题。 @TyCobb 数十万行。 sources 有 200 个点,targets 有 200 个点,它在并行模式下分析 3.2 秒,在顺序计算下分析 2.9 秒。 @DourHighArch 一个非常足智多谋的答案,有助于调查问题

标签: c# multithreading parallel-processing thread-safety thread-local


【解决方案1】:

如果没有可靠地重现问题的良好Minimal, Complete, and Verifiable code example,就不可能提供明确的答案。您发布的代码甚至看起来都不是真实代码的摘录,因为声明为方法返回类型的类型与return 语句实际返回的值不同。

但是,您发布的代码显然不能很好地使用Parallel.For()。您的 Line 构造函数要证明并行创建项目的任务是相当昂贵的。需要明确的是,这是这里唯一可能的胜利。

最后,您仍然需要将您创建的所有Line 实例聚合到一个列表中,因此为Parallel.For() 任务创建的所有这些中间列表只是纯粹的开销。并且聚合必然是序列化的(即一次只有一个线程可以将一个项目添加到result 集合中),并且以最坏的方式(每个线程在放弃锁定之前只能添加一个项目和另一个线程有机会抓住它)。

坦率地说,您最好将每个本地 List&lt;T&gt; 存储在一个集合中,然后在 Parallel.For() 返回后在主线程中一次聚合它们。并不是说这可能会使代码比直接的非并行实现更好。但至少它不太可能变得更糟。 :)

最重要的是,您似乎没有可以从并行化中受益的工作负载。如果您不这么认为,则需要以更清晰、更详细的方式解释该想法的基础。

如果我使用一个通用 List 内核会中止操作并引发异常,有人能说出原因吗?

您已经在使用(看起来)List&lt;T&gt; 作为每个任务的本地数据,确实应该没问题,因为任务不共享它们的本地数据。

但是,如果您问为什么尝试使用List&lt;T&gt; 而不是ConcurrentBag&lt;T&gt; 作为result 变量时会出现异常,那么这完全是意料之中的。 List&lt;T&gt; 类不是线程安全的,但Parallel.For() 将允许它运行的每个任务与所有其他任务同时执行localFinally 委托。所以你有多个线程都试图同时修改同一个非线程安全的集合。这是灾难的秘诀。你很幸运你得到了例外;实际行为是未定义的,您很可能会简单地破坏数据结构,导致运行时异常。

【讨论】:

  • 我更新了问题。为了更好地解释我为什么要看并行计算:我有两个列表,都有数百个点;我想将List1 中的每个点与List2 中的每个点连接起来。由于这会生成数十万行,我认为如果每一行都可以是一个独立的Task,并且每个任务分派到不同的线程以拆分工作负载,这会加快速度。现在,我可以在输入列表中看到一个无害的读/读冲突,在输出列表中看到一个写/写冲突,我认为这是通过ConcurrentBag 解决的。
  • “我认为用ConcurrentBag 解决了”——取决于你对“解决”的定义。使用线程安全的集合类型可确保数据结构在并发使用时保持不损坏。但它也会序列化对它的所有访问,并且由于您的同步粒度是每个元素一次,因此可能存在大量争用开销。而且,创建单个元素的成本不足以让所有这些开销,无论是在单个任务中,还是在最终聚合中,都是值得的。所有这些我都在上面解释过。
猜你喜欢
  • 1970-01-01
  • 2021-10-11
  • 1970-01-01
  • 2019-09-16
  • 1970-01-01
  • 2010-11-28
  • 1970-01-01
  • 1970-01-01
  • 2017-02-21
相关资源
最近更新 更多