【问题标题】:Advice on starting a large multi-threaded programming project关于启动大型多线程编程项目的建议
【发布时间】:2010-12-26 12:08:21
【问题描述】:

我的公司目前正在运行一个第三方模拟程序(自然灾害风险建模),该程序从磁盘中提取数 GB 的数据,然后运行几天以产生结果。我很快会被要求将其重写为多线程应用程序,以便它在数小时而不是数天内运行。我预计有大约 6 个月的时间来完成转换,并且将独自工作。

我们有一个 24-proc 的盒子来运行它。我将可以访问原始程序的源代码(我认为是用 C++ 编写的),但目前我对它的设计方式知之甚少。

我需要有关如何解决此问题的建议。我是一位经验丰富的程序员(约 30 年,目前使用 C# 3.5),但没有多处理器/多线程经验。如果合适的话,我愿意并且渴望学习一门新的语言。我正在寻找有关语言、学习资源、书籍、架构指南的建议。等等

要求:Windows 操作系统。具有大量支持和良好学习资源的商业级编译器。不需要花哨的 GUI - 它可能会从配置文件运行并将结果放入 SQL Server 数据库。

编辑:当前的应用程序是 C++,但我几乎肯定不会使用该语言进行重写。我删除了某人添加的 C++ 标签。

【问题讨论】:

  • 你打算重写原版还是只是维护?
  • 你能提供更多关于计算本身的信息吗?这段代码做了什么样的计算?它是用什么语言写的?什么样的数据结构?我们在谈论什么数值方法?代码多长?
  • the only authority on concurrence imo。不幸的是,我没有跟上课程页面,我不能保证这个页面的状态[因此评论,不回答]。拿理论,应用到C#。
  • 在设计如此复杂的系统时学习一门新语言并不是一个好的选择。
  • 为什么标记为 C++?这是项目的要求之一吗?否则,请坚持使用您熟悉的语言。没有必要让自己变得更加困难。

标签: multithreading architecture simulation parallel-processing


【解决方案1】:

数值过程模拟通常在单个离散问题网格(例如surface of the Earthclouds of gas and dust)上运行,这通常排除simple task farming 或并发方法。这是因为划分为一组表示物理空间区域的处理器的网格不是一组独立的任务。每个子网格边缘的网格单元需要根据存储在其他处理器上的网格单元的值进行更新,这些单元在逻辑空间上是相邻的。

high-performance computing 中,模拟通常是parallelised,使用MPIOpenMP。 MPI 是一个消息传递库,绑定了多种语言,包括C, C++, FortranPythonC#。 OpenMP 是一种用于共享内存多处理的 API。一般来说,MPI 比 OpenMP 更难编码,更具侵入性,但也更灵活。 OpenMP 需要在处理器之间共享内存区域,因此不适合许多架构。 Hybrid schemes 也是可以的。

这种类型的编程有其自身的特殊挑战。除了race conditionsdeadlockslivelocks 以及concurrent programming 的所有其他乐趣之外,您还需要考虑处理器网格的topology - 如何选择在物理处理器之间拆分逻辑网格.这很重要,因为您的并行speedup 是处理器之间通信量的函数,它本身是分解网格的总边长的函数。随着您添加更多处理器,此表面积会增加,从而增加communication overhead 的数量。增加granularity 最终会变得令人望而却步。

另一个重要的考虑因素是可以并行化的代码比例。 Amdahl's law 然后规定理论上可达到的最大加速。在开始编写任何代码之前,您应该能够估计这一点。

这两个事实将共同限制您可以运行的最大处理器数量。甜蜜点可能比您想象的要低得多。

我推荐这本书High Performance Computing,如果你能掌握的话。特别是关于性能基准测试和调优的章节是无价的。

来自Lawerence Livermore National Laboratory 的介绍是涵盖主要问题的并行计算的优秀在线概述。

【讨论】:

    【解决方案2】:

    您在多线程项目中的最大问题是线程间可见的状态过多 - 编写以不安全方式读取/更改数据的代码太容易了,尤其是在缓存一致性等问题较弱的多处理器环境中一致的记忆等可能会发挥作用。

    调试竞态条件显然令人不快。

    如果您正在考虑在网络上的多台机器上分配您的工作,请按照您的想法处理您的设计:也就是说,确定哪些任务可以并行发生,每个任务的输入是什么,每个任务的输出是什么任务是,以及在给定任务开始之前必须完成哪些任务。练习的重点是确保数据对另一个线程可见的每个位置以及产生新线程的每个位置都经过仔细考虑。

    一旦这样的初始设计完成,数据的所有权就会有明确的划分,以及所有权的取得/转移的明确点;因此,您将处于一个非常有利的位置,可以安全地利用多线程为您提供的可能性——廉价的共享数据、廉价的同步、无锁的共享数据结构。

    【讨论】:

    • 就像托尼·霍尔所说:“不要通过分享来交流,通过交流来分享。”除了几个队列之外,在给定时间应该有可能只有一个线程作用于一条数据。尝试通过强制执行这种做法的“通道”查找 go 编程语言方法(但不要使用 go,它仅用于播放)。
    【解决方案3】:

    如果您可以将工作负载拆分为不依赖的工作块(即数据集可以按位处理,没有很多数据依赖项),那么我会使用线程池/任务机制.大概任何 C# 都相当于 Java 的 java.util.concurrent。我会从数据中创建工作单元,并将它们包装在一个任务中,然后将这些任务扔到线程池中。

    当然,性能在这里可能是必需的。如果您可以保持原始处理代码内核不变,那么您可以在您的 C# 应用程序中调用它。

    如果代码有很多数据依赖项,那么将其分解为线程任务可能会更加困难,但您可以将其分解为操作管道。这意味着线程 1 将数据传递给线程 2,线程 2 将数据传递给线程 3 到 8,线程 8 将数据传递给线程 9,等等。

    如果代码包含大量浮点数学,则可能值得考虑用 OpenCL 或 CUDA 重写,并在 GPU 而不是 CPU 上运行。

    【讨论】:

    • +1:重读问题后,我正要按照这些思路写一些东西。
    【解决方案4】:

    对于一个为期 6 个月的项目,我会说首先开始阅读一本关于该主题的好书肯定是值得的。我建议Joe Duffy's Concurrent Programming on Windows。这是我所知道的关于该主题的最详尽的书,它涵盖了 .NET 和本机 Win32 线程。当我发现这个 gem 时,我已经编写了 10 年的多线程程序,但仍然在几乎每一章中都发现了我不知道的东西。

    此外,“自然灾害风险建模”听起来像是很多数学题。也许你应该看看英特尔的 IPP 库:它为许多常见的低级数学和信号处理算法提供了原语。它支持开箱即用的多线程,这可能会使您的任务变得更加容易。

    【讨论】:

      【解决方案5】:

      如果你为它设计项目,有很多技术可以用来处理多线程。

      最普遍和普遍的就是“避免共享状态”。尽可能在线程之间复制资源,而不是让它们访问同一个共享副本。

      如果您自己编写低级同步代码,您必须记住绝对不要做任何假设。编译器和 CPU 都可能重新排序您的代码,从而在读取代码时创建竞争条件或死锁,而这些情况似乎都不可能发生。防止这种情况的唯一方法是使用内存屏障。请记住,即使是最简单的操作也可能会出现线程问题。像++i 这样简单的东西通常不是原子的,如果多个线程访问i,你会得到不可预知的结果。 当然,仅仅因为您已经为变量分配了一个值,这并不能保证新值对其​​他线程是可见的。编译器可能会推迟实际将其写入内存。同样,内存屏障强制它“刷新”所有待处理的内存 I/O。

      如果我是你,如果可能的话,我会使用比简单的锁/互斥锁/监视器/关键部分更高级别的同步模型。有一些 CSP 库可用于大多数语言和平台,包括 .NET 语言和本机 C++。

      这通常会使竞争条件和死锁难以检测和修复,并允许可笑的可扩展性。但是这种范式也有一定的开销,因此每个线程完成的工作可能比使用其他技术时要少。它还要求整个应用程序专门针对这种范式进行结构化(因此改造现有代码很棘手,但由于您是从头开始,所以问题不大——但您仍然不熟悉)

      另一种方法可能是Transactional Memory。这更容易适应传统的程序结构,但也有一些限制,而且我不知道有多少生产质量的库用于它(STM.NET 最近发布,可能值得一试。英特尔有一个 C++ STM 扩展的编译器也内置在语言中)

      但无论您使用哪种方法,您都必须仔细考虑如何将工作拆分为独立的任务,以及如何避免线程之间的串扰。任何时候两个线程访问同一个变量,你都有一个潜在的错误。并且任何时候两个线程访问同一个变量或只是同一地址附近的另一个变量(例如,数组中的下一个或前一个元素),数据将不得不在核心之间交换,迫使它从 CPU 缓存刷新到内存,然后读入另一个内核的缓存。这可能会对性能造成重大影响。

      哦,如果您确实使用 C++ 编写应用程序,请不要低估该语言。您必须先详细学习该语言,然后才能编写健壮的代码,更不用说健壮的线程代码了。

      【讨论】:

        【解决方案6】:

        在这种情况下,我们所做的一件对我们非常有效的事情是将要完成的工作分解为单独的块,并将每个块上的操作分解到不同的处理器中。然后我们有处理器链,数据块可以独立地通过这些链工作。链中的每组处理器都可以在多个线程上运行,并且可以根据自己相对于链中其他处理器的性能来处理更多或更少的数据。

        还将数据和操作都分解成更小的部分,使应用更易于维护和测试。

        【讨论】:

          【解决方案7】:

          这里可以提供很多具体的个人建议,并且已经有几个人这样做了。 但是,没有人能准确地告诉您如何使这一切满足您的特定要求(您甚至还不完全了解自己),所以我强烈建议您现在阅读HPC (High Performance Computing) 以了解总体情况概念清晰,更清楚哪个方向最适合您的需求。

          【讨论】:

            【解决方案8】:

            您选择使用的模型将取决于您的数据结构。您的数据是紧耦合还是松耦合?如果您的模拟数据是紧密耦合的,那么您将需要查看 OpenMP 或 MPI(并行计算)。如果您的数据是松散耦合的,那么作业池可能更适合......甚至可能使用分布式计算方法。

            我的建议是获取并阅读介绍性文本,以熟悉并发/并行的各种模型。然后查看您的应用程序的需求并决定您将需要使用哪种架构。在您知道您需要哪种架构之后,您可以查看工具来帮助您。

            作为该主题介绍的一本相当高评价的书是“并发的艺术:线程猴子编写并行应用程序的指南”。

            【讨论】:

              【解决方案9】:

              阅读有关 Erlang 和“演员模型”的特别信息。如果您将所有数据都设为不可变,那么您将更容易将其并行化。

              【讨论】:

              • 为什么不呢?你仍然可以模仿一些使 Erlang 非常适合的技巧
              • 当然,他在问题中说他对选择语言的建议很感兴趣。所以绝对是我的 +1
              • Erlang 没有使用多线程来提高性能,而是使用它来简化程序的数据流建模。对于他只追求速度的问题,这将是完全错误的选择
              【解决方案10】:

              大多数其他答案都提供了有关划分项目的好建议 - 寻找可以并行执行且只需要很少的数据共享的任务。请注意非线程安全的构造,例如静态或全局变量,或非线程安全的库。我们遇到的最糟糕的是TNT 库,它在某些情况下甚至不允许线程安全读取。

              与所有优化一样,首先关注瓶颈,因为线程会增加很多复杂性,您希望在不必要的地方避免它。

              您需要很好地掌握各种线程原语(互斥体、信号量、临界区、条件等)及其有用的情况。

              如果您打算继续使用 C++,我要补充一点,那就是我们在使用 boost.thread 库方面取得了很大的成功。它提供了大多数所需的多线程原语,尽管确实缺少线程池(我会警惕可以通过 google 找到的非官方“增强”线程池,因为它存在许多死锁问题)。

              【讨论】:

                【解决方案11】:

                我会考虑在 .NET 4.0 中这样做,因为它有很多新的支持,专门针对简化并发代码的编写。它的正式发布日期是 2010 年 3 月 22 日,但在此之前它可能会 RTM,您现在可以从相当稳定的 Beta 2 开始。

                您可以使用您更熟悉的 C#,也可以使用托管 C++。

                在高层次上,尝试将程序分解为System.Threading.Tasks.Task,它们是单独的工作单元。此外,我会尽量减少共享状态的使用,并在可能的情况下考虑使用Parallel.For(或ForEach)和/或PLINQ

                如果您这样做,很多繁重的工作将以一种非常有效的方式为您完成。这是微软将越来越多地支持的方向。

                2:我会考虑在 .NET 4.0 中执行此操作,因为它有很多新的支持,专门用于更轻松地编写并发代码。它的正式发布日期是 2010 年 3 月 22 日,但在此之前它可能会 RTM,您现在可以从相当稳定的 Beta 2 开始。在高层次上,尝试将程序分解为System.Threading.Tasks.Task,它们是单独的工作单元。此外,我会尽量减少共享状态的使用,并尽可能考虑使用 Parallel.For 和/或 PLINQ。如果你这样做,许多繁重的工作将以一种非常有效的方式为你完成。 1:http://msdn.microsoft.com/en-us/library/dd321424%28VS.100%29.aspx

                【讨论】:

                • 尽管 .Net 平台取得了巨大成功,但不建议使用此新版本,因为此决定可能会带来确保其稳定性的风险。
                【解决方案12】:

                对不起,我只是想在这里添加一个悲观或更现实的答案。

                您面临时间压力。 6 个月的最后期限,你甚至不知道这个系统是什么语言,它做什么以及它是如何组织的。如果这不是一个微不足道的计算,那么这是一个非常糟糕的开始。

                最重要的是:你说你以前从未做过多线程编程。这是我一次响起4个闹钟的地方。多线程是困难的,当你想把它做好时需要很长时间来学习它——当你想赢得巨大的速度提升时,你需要把它做对。即使使用像 Total Views 调试器或 Intels VTune 这样的好工具,调试也非常令人讨厌。

                然后你说你想用另一种语言重写应用程序 - 这并不像你必须重写它那么糟糕。无需重新设计即可将单线程程序转变为运行良好的多线程程序的机会几乎为零。

                但是学习多线程和一门新语言(你的 C++ 技能是什么?),时间线为 3 个月(你必须编写一个扔掉的原型 - 所以我将时间跨度分成两半)是极具挑战性的。

                我的建议很简单,并且不会喜欢它:现在学习多线程——因为它是未来必备的技能——但是把这份工作留给已经有经验的人。好吧,除非您不关心该计划是否成功,而只是在寻找 6 个月的付款。

                【讨论】:

                  【解决方案13】:

                  如果可以让所有线程处理不相交的进程数据集,并将其他信息存储在 SQL 数据库中,那么您可以很容易地在 C++ 中完成,并且只需生成新线程来处理它们自己的部分使用 Windows API。 SQL 服务器将通过其 DB 事务处理所有硬同步魔法!当然,C++ 的执行速度会比 C# 快很多。

                  您绝对应该为此任务修改 C++,并了解 C++ 代码,并在现有代码中查找效率错误以及添加多线程功能。

                  【讨论】:

                  • 我不讨厌这个以至于不能投反对票,但是推荐 SQL 数据库用于任何涉及高性能的事情?至少 -0.5。
                  【解决方案14】:

                  您已将此问题标记为 C++,但提到您目前是 C# 开发人员,因此我不确定您是否会从 C++ 或 C# 处理此任务。无论如何,如果您要使用 C# 或 .NET(包括 C++/CLI):我已将以下 MSDN 文章添加为书签,强烈建议您在准备工作中通读它。

                  Calling Synchronous Methods Asynchronously

                  【讨论】:

                    【解决方案15】:

                    无论您要编写什么技术,请查看this must read book on concurrency "Concurrent programming in Java",对于.Net,我强烈推荐retlang library 用于并发应用程序。

                    【讨论】:

                      【解决方案16】:

                      我不知道它是否被提及,但如果我站在你的立场上,我现在要做的(除了阅读此处发布的每个答案)是在你最喜欢的(大多数使用)语言。

                      我没有丰富的多线程经验。我过去玩过它是为了好玩,但我认为获得一些使用一次性应用程序的经验将适合您未来的工作。

                      祝你在这项工作中好运,我必须承认我希望我有机会从事这样的工作......

                      【讨论】:

                        猜你喜欢
                        • 1970-01-01
                        • 2016-03-13
                        • 1970-01-01
                        • 2017-02-01
                        • 1970-01-01
                        • 2011-02-25
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多