【问题标题】:How many threads does it take to make them a bad choice?需要多少线程才能使它们成为一个糟糕的选择?
【发布时间】:2010-11-29 00:16:38
【问题描述】:

我必须使用 boost::thread 用 C++ 编写一个不太大的程序。

目前的问题是处理大量(可能是数千或数万个。也可能是成百上千)数量的(可能)大文件。每个文件都相互独立,并且它们都驻留在同一个目录中。我正在考虑使用多线程方法,但问题是,我应该使用多少线程?我的意思是,什么数量级? 10、500、12400?

有一些同步问题,每个线程应该返回一个值结构(为每个文件累积),然后将这些值添加到“全局”结构中以获取整体数据。我意识到有些线程可能会因为同步而“饿死”,但如果它只是一个添加操作,这有关系吗?

我在想

for(each file f in directory){

    if (N < max_threads)//N is a static variable controlling amount of threads
         thread_process(f)
    else
       sleep()
}

这是在 HP - UX 中,但我无法经常测试它,因为它是一个远程且完全无法访问的服务器。

【问题讨论】:

  • 3. 1 做某事加上 1 以非原子方式添加到另一个。
  • 这完全取决于您的硬件以及您正在执行的处理类型。它是受 CPU 限制还是受 IO 限制?如果 CPU 受限,您有多少个 CPU 内核?如果 IO 受限,您在什么时候超过了设备的吞吐量限制?

标签: c++ boost multithreading performance


【解决方案1】:

根据 Herb Sutter 在his article 中讨论的 Amdahl 定律:

程序的某些处理量是完全“O(N)”可并行化的(称为这部分 p),并且只有该部分可以直接在具有越来越多处理器内核的机器上扩展。该程序的其余工作是“O(1)”顺序(s)。 [1,2] 假设完美使用所有可用内核且没有并行化开销,阿姆达尔定律指出,在具有 N 个内核的机器上,该程序工作负载的最佳加速速度由

给出

在您的情况下,I/O 操作可能需要大部分时间,以及同步问题。您可以计算将花费在阻塞(?)慢 I/O 操作上的时间,并大致找到适合您的任务的线程数。


Herb Sutter 的并发相关文章的完整列表可以在 here 找到。

【讨论】:

  • 很好的链接!请注意,本文还继续,例如:尽量减少顺序性(例如通过将数据多路复用到一个文件中)。
  • 添加了指向所有 Sutter 文章的链接。它们与 OP 的问题没有直接关系,但仍有帮助。
  • 问题是这假定其他线程没有开销。几乎从来没有这种情况,我会说每个核心拥有超过 1 个线程并不是很有效(只要该线程始终处于负载状态)。 (续...)
  • 如果您有一个线程在内核上闲置了一段时间(例如一个线程等待输入),那么向该内核添加另一个线程可能是值得的。此外,核心运行的线程越多,它就越需要移动缓存,从而导致缓存未命中,这将真正减慢它的速度。虽然这是一个很好的“法律”(我认为按照科学标准,它真的不能这样称呼),但它在现实世界中并不成立。
  • 在这种特殊情况下,您还需要应对硬盘访问,这将是您真正的限制因素。我建议您有一个专门用于将文件读入内存的线程,然后将它们交给其他线程处理,否则,如果您尝试从单独的线程上读取文件,硬盘将开始到处乱跳。这将大大降低硬盘的读取速度,从而导致更长的处理时间。 (事后看来,我可能应该将此作为答案发布,但是哦,好吧,我没有代表)。
【解决方案2】:

我不太确定 HP/UX,但在 Windows 世界中,我们使用线程池来解决此类问题。 Raymond Chen wrote about this 前阵子,其实……

最简单的一点是,如果线程数超过系统中 CPU 内核数的 2 倍左右,我通常不会期望任何东西在 CPU 密集型负载上能够很好地扩展。对于 I/O 绑定负载,您可能能够摆脱更多,这取决于您的磁盘子系统有多快,但是一旦达到大约 100 左右,我会认真考虑更改模型...

【讨论】:

  • 线程太多也会弄乱你的IO。磁盘磁头抖动和缓冲区争用将克服运行大量进程的好处。我会坚持使用久经考验的“cpu cores * 2”公式。
  • @James,对,对。但是我们不是都拥有那些新奇的英特尔 SSD 之一吗?哦,没错。 Newegg 抬高了它们的价格,所以我买不起 :(
  • 我还要补充一点,线程过多还有另一个副作用,至少在 *nix 系统上是这样。分配给线程的存储有时会从缓冲池中被窃取,从而进一步减慢 I/O。
  • 是的 - 如果您将堆栈大小设置为 2 兆字节,那么 每个线程 都有一个 2 兆字节的堆栈。这可能会使您的进程内存使用量激增,从而导致更多的分页和碎片,即使堆栈并未全部被彻底使用。 Java 应用程序通常可以将更多线程放入较小的进程地址空间,因为一切都在堆上,但 C 或 C++ 应用程序可能需要更深的堆栈,因为具有自动存储的变量/缓冲区。我不确定这是否适用于 Win32。
【解决方案3】:

您说文件都在一个目录中。这是否意味着它们都在一个物理驱动器上?

如果是这样,并且假设它们还没有被缓存,那么你的工作就是让单个读取头保持忙碌,并且再多的线程也无济于事。事实上,如果它由于并行性而不得不在轨道之间跳跃,你可以减慢它的速度。

另一方面,如果计算部分花费大量时间,导致读取头必须等待,那么拥有 >1 个线程可能是有意义的。

通常,使用线程来提高性能是没有意义的,除非它可以让并行的硬件同时工作。

更多时候,线程的价值在于,例如,跟踪多个同时进行的对话,例如如果您有多个用户,每个线程都可以等待自己的 Johny 或 Suzy 而不会感到困惑。

【讨论】:

  • +1 第一个答案基本上是说“从 1 开始并从那里测量”^^
【解决方案4】:

要详细说明它真的取决于

IO boundedness of the problem
    how big are the files
    how contiguous are the files
    in what order must they be processed
    can you determine the disk placement
how much concurrency you can get in the "global structure insert"
    can you "silo" the data structure with a consolidation wrapper
the actual CPU cost of the "global structure insert" 

例如,如果您的文件驻留在 3 TB 闪存阵列上,则解决方案与它们驻留在单个磁盘上的解决方案不同(如果“全局结构插入”所需的读取时间少于读取时间,则问题是 I/O bounded你也可以有一个带有 2 个线程的 2 级管道 - 读取级为插入级提供数据。)

但在这两种情况下,架构都可能是 2 个阶段的垂直管道。 n 个读线程和 m 个写线程,其中 n 和 m 由阶段的“自然并发”决定。

为每个文件创建一个线程可能会导致磁盘抖动。就像您将 CPU 绑定进程的线程数定制为自然可实现的 CPU 并发性(并在上面创建上下文切换开销 AKA 抖动)一样,在 I/O 端也是如此 - 从某种意义上说,您可以想到磁盘抖动为“磁盘上的上下文切换”。

【讨论】:

    【解决方案5】:

    如果工作负载与听起来一样接近I/O 界限,那么您可能会通过与主轴数量一样多的线程获得最大吞吐量。如果您有多个磁盘并且所有数据都在同一个RAID 0 上,那么您可能只需要一个线程。如果多个线程试图访问磁盘的非连续部分,操作系统必须停止读取一个文件,即使它可能就在头部下方,并移动到磁盘的另一部分以服务另一个线程,以便它不会饿死。只有一个线程,磁盘永远不需要停止读取来移动磁头。

    显然,这取决于非常线性的访问模式(例如视频重新编码)以及数据实际上在磁盘上未分段,这在很大程度上取决于。如果工作负载更多地受 CPU 限制,那么它不会那么重要,您可以使用更多线程,因为磁盘无论如何都会旋转它的拇指。

    正如其他海报所建议的那样,首先是个人资料!

    【讨论】:

    • 即使每个主轴有一个线程,如果所有文件实际上都在同一个磁盘上(它们在同一个目录中),那么你不会得到那么多帮助。如果文件在 RAID(或逻辑卷)上,更不用说在 SAN 系统上可用,那么您最好每个主轴至少使用一个 - 在您的线程和原始磁盘之间有足够的东西,可以让它纯粹猜测如何使用许多线程。
    • 这还取决于您的 I/O 调度程序的有效性。如果您有 10 个 I/O 请求要发出,您可能会受益于一次发出所有请求,以便在磁盘轴上实现高效的电梯调度程序。如果您一次只发送一个请求,I/O 子系统将完全帮不了您,您将扼杀所有可能的并行机会。
    【解决方案6】:

    使用线程池而不是为每个文件创建线程。编写解决方案后,您可以轻松调整线程数。如果作业彼此独立,我会说线程数应该等于核心数/CPU。

    【讨论】:

      【解决方案7】:

      听起来并不老套,但您可以根据需要使用尽可能多的线程。

      基本上,您可以绘制线程数与(实际)完成时间的关系图。您还可以绘制总线程数与总线程时间的关系。

      特别是第一张图表将帮助您确定 CPU 能力的瓶颈所在。在某些时候,您将成为I/O 绑定(意味着磁盘无法足够快地加载数据),或者线程数将变得如此之大以至于影响机器的性能。

      第二个确实发生了。我看到一段代码最终创建了 30,000 多个线程。最终通过将其限制为 1,000 来更快。

      另一种看待这个问题的方式是:多快才足够快? I/O 成为瓶颈是一回事,但您可能会在此之前达到“足够快”的点。

      【讨论】:

      • “足够快”这一点很好。我已经避免编写缓存机制,例如通过充分利用线程来并行计算不同的值。缓存会更快吗?当然可以,但是重新计算节省了磁盘空间,无论如何仍然可以在几秒钟内完成,而且对于一次性操作来说,这“足够快”。
      【解决方案8】:

      答案在一定程度上取决于您需要对每个文件执行的处理的 CPU 密集程度。

      在处理时间占主导地位I/O 时间的一个极端情况下,线程为您带来的好处就是能够利用多核(可能还有超线程)来利用 CPU 的最大可用处理能力.在这种情况下,您希望工作线程的数量大致等于系统上逻辑核心的数量。

      在另一个极端情况下,I/O 是您的瓶颈,您不会看到多线程带来的所有好处,因为它们大部分时间都在休眠等待 I/O 完成。在这种情况下,您需要专注于最大化 I/O 吞吐量而不是 CPU 利用率。在单个未碎片化的硬盘驱动器或 DVD 上,如果您受 I/O 限制,具有多个线程可能会损害性能,因为您将从单个线程上的顺序读取中获得最大的 I/O 吞吐量。如果驱动器有碎片,或者您有 RAID 阵列或类似设备,那么同时运行多个 I/O 请求可能会提高您的 I/O 吞吐量,因为控制器可能能够智能地重新排列它们以进行更有效的读取。

      我认为将其视为两个独立的问题可能会有所帮助。一个是如何为文件读取获得最大的 I/O 吞吐量,另一个是如何最大限度地利用 CPU 来处理文件。您可能会通过让少量 I/O 线程启动 I/O 请求和一个工作线程池来获得最佳吞吐量,这些工作线程池大致等于处理可用数据时的逻辑 CPU 内核的数量。不过,是否值得努力实现这样更复杂的设置取决于您的特定问题的瓶颈所在。

      【讨论】:

      • 我觉得你说得比我好。
      【解决方案9】:

      这听起来可能有点过时,但您是否考虑过简单地分叉进程?听起来您拥有高度独立的工作单元,并且返回数据的聚合很小。进程模型还可以释放虚拟地址空间(如果您在 32 位机器上,这可能会很紧张),允许每个工作室说 mmap() 正在处理的整个文件。

      【讨论】:

      • 其实我有,还没有丢弃。 mmap() 听起来很有吸引力。但是,我必须找到一种方法让进程将进程数据返回到“主”进程,并且我可能同时有太多进程处于活动状态
      【解决方案10】:

      有很多变量会影响性能(操作系统、文件系统、硬盘速度与 CPU 速度、数据访问模式、读取数据后对数据进行了多少处理等)。

      因此,您最好的选择是简单地尝试在具有代表性的数据集(尽可能大的数据集,以便文件系统缓存不会严重扭曲结果)上针对每个可能的线程数进行测试运行,并记录多长时间每次都需要。从一个线程开始,然后用两个线程再试一次,依此类推,直到你觉得你有足够的数据点。最后,您应该将数据绘制成一条漂亮的曲线,指示“最佳位置”在哪里。您应该能够在循环中执行此操作,以便在一夜之间自动编译结果。

      【讨论】:

        【解决方案11】:

        更多线程不一定会给您带来更高的吞吐量。线程的创建(在 CPU 时间和操作系统资源方面)和运行(在内存和调度方面)都有不小的成本。而且您拥有的线程越多,与其他线程争用的可能性就越大。添加线程有时甚至会减慢执行速度。每个问题都存在细微差别,您最好编写一个漂亮、灵活的解决方案并试验参数,看看哪种方法效果最好。

        您的示例代码为每个文件生成一个线程,几乎会立即将系统淹没max_threads 的值超过 10 左右。正如其他人所建议的那样,您可能想要一个带有工作队列的线程池。每个文件都是独立的这一事实很好,因为这使得它几乎令人尴尬地并行(除了每个工作单元结束时的聚合)。

        一些会影响您的吞吐量的因素:

        • CPU 内核数
        • 磁盘通道数(主轴、RAID 设备等)
        • 处理算法,以及问题是 CPU 还是 I/O 受限
        • 争用主统计结构

        去年,我编写了一个与您描述的基本相同的应用程序。我最终使用了 Python 和 pprocess library。它使用带有工作进程池的多进程模型,通过管道(而不是线程)进行通信。主进程将读取工作队列,将输入分成块,并将块信息发送给工作人员。工作人员会处理数据,收集统计信息,并在完成后将结果发送给主服务器。 master 会将结果与全局总数结合起来,然后将另一个块发送给 worker。我发现它几乎线性扩展到 8 个工作线程(在 8 核机器上,这非常好),除此之外它还退化了。

        需要考虑的一些事项:

        • 使用带有工作队列的线程池,其中线程数可能与系统中的内核数差不多
        • 或者,使用多进程设置,通过管道进行通信
        • 评估使用mmap()(或等效项)对输入文件进行内存映射,但前提是您已对基线案例进行了概要分析
        • 以块大小的倍数(例如 4kB)读取数据,并在内存中分割成行
        • 从一开始就构建详细的日志记录,以帮助调试
        • 在更新主统计数据时要注意争用,尽管它可能会被数据的处理和读取时间淹没
        • 不要做假设 - 测试和衡量
        • 设置尽可能接近部署系统的本地开发环境
        • 使用数据库(如SQLite)获取状态数据、处理结果等
        • 数据库可以跟踪哪些文件已被处理,哪些行有错误、警告等
        • 仅授予您的应用对原始目录和文件的只读访问权限,并将结果写入其他位置
        • 注意不要尝试处理由另一个进程打开的文件(这里有一些技巧)
        • 请注意不要达到操作系统对每个目录文件数的限制
        • 配置所有内容,但请确保一次只更改一件事,并保留详细记录。性能优化困难
        • 设置脚本,以便您可以始终如一地重新运行测试。拥有一个数据库在这里会有所帮助,因为您可以删除将文件标记为已处理并针对相同数据重新运行的记录。

        当您描述的一个目录中有大量文件时,除了可能达到文件系统限制外,是时候统计该目录并确定您已经处理了哪些文件以及您仍需要处理哪些文件显着地。例如,考虑按日期将文件分解为子目录。

        关于性能分析的另一个词:在将性能从小型测试数据集外推到超大型数据集时要小心。你不能。我发现很难达到某个点,即我们每天​​在编程中所做的关于资源的常规假设不再成立。例如,当我的应用程序超过它时,我才发现 MySQL 中的语句缓冲区是 16MB!并且保持 8 个内核繁忙可能会占用大量内存,但如果您不小心的话,您可以轻松地吃掉 2GB 的 RAM!在某些时候,您必须在生产系统上测试真实数据,但要给自己一个安全的测试沙箱来运行,这样您就不会修改生产数据或文件。

        与本次讨论直接相关的是 Tim Bray 博客上的一系列文章,称为 "Wide Finder" project。问题只是解析日志文件并生成一些简单的统计信息,但在多核系统上以最快的方式。许多人以多种语言提供了解决方案。绝对值得一读。

        【讨论】:

          【解决方案12】:

          我同意每个人建议的线程池:您使用池调度任务,池分配线程来执行任务。

          如果您受 CPU 限制,只要 CPU 使用率低于 100%,只需继续添加线程即可。当您绑定I/O 时,磁盘抖动可能会在某些时候阻止更多线程提高速度。那你必须自己去发现。

          你见过英特尔的Threading Building Blocks吗?请注意,我无法评论这是否是您需要的。我只在 Windows 上做了一个小玩具项目,那是几年前的事了。 (顺便说一句,它和你的有点相似:它递归地遍历文件夹层次结构并计算它找到的源代码文件中的行数。)

          【讨论】:

          • 在 CPU 达到 100% 之前添加线程可能不是最好的主意 - 过多的线程争用可能会降低缓存性能。但最好提一下 TBB。
          【解决方案13】:

          最简单的线程有多昂贵取决于操作系统(您可能还需要调整一些操作系统参数以通过一定数量的线程)。至少每个都有自己的 CPU 状态(寄存器/标志,包括浮点)和堆栈以及任何特定于线程的堆存储。

          如果每个单独的线程不需要太多不同的状态,那么您可以通过使用较小的堆栈大小来相当便宜地获得它们。

          在极限情况下,您可能最终需要使用非操作系统协作线程机制,甚至自己使用微小的“执行上下文”对象来多路复用事件。

          从线程开始,以后再担心:)

          【讨论】:

            【解决方案14】:

            作为一个大概数字,您应该将线程数保持在 10 到 100 之间,以最大限度地减少锁争用和上下文切换开销。

            【讨论】:

              【解决方案15】:

              这里有两个问题,第一个是您对用于处理大量文件的理想线程数的问题,第二个是如何获得最佳性能。

              让我们从第二个问题开始,首先我不会对每个文件进行并行化,但我会一次对一个文件进行并行化处理。这将对您环境的多个部分有很大帮助: - 硬盘驱动器,因为它不必从一个文件中寻找 n - 1 个其他文件 - 操作系统文件缓存将使用您所有线程上所需的数据保持温暖,您不会遇到太多的缓存垃圾。

              我承认并行化您的应用程序的代码会稍微复杂一些,但您将获得的好处是显着的。

              由此,您的问题的答案很简单,您应该为系统中的每个内核最多匹配一个线程。这将使您能够尊重您的缓存,并最终在您的系统上实现最佳性能。

              当然,最终的一点是,使用这种类型的处理,您的应用程序将更加尊重您的系统,因为同时访问 n 个文件可能会使您的操作系统无响应。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2020-10-02
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-01-11
                • 1970-01-01
                相关资源
                最近更新 更多