【问题标题】:Multithreading Pool in C++C++中的多线程池
【发布时间】:2019-08-08 13:00:47
【问题描述】:

我正在开发一个程序,它不断地从视频流中接收帧并计算每对帧之间的运动估计值。

由于硬件限制,我必须在 CPU 中计算运动估计 (ME) 算法,每次计算大约需要 2 秒。正因为如此,我想用多线程来实现 ME 算法。想法是在主线程中从流中接收下一帧,同时在其他线程中计算运动值。

我已经为每个任务使用一个线程来完成它,也就是说,每次收到一对帧时,我都会创建一个新线程来计算运动值。但是,由于运动计算的时间过长,会创建很多线程并同时运行,我认为这不是很有效。

我认为重新实现这一点的最佳方法是使用线程池。例如,一方面有一个主线程接收帧并将它们存储在缓冲区或队列中,另一方面有 4 或 8 个线程同时运行并从接收缓冲区读取,如果我没记错的话应该受到保护通过互斥锁。但是,主线程接收帧的速度比一个运动计算结束要快得多,我不知道如何管理。

我对 C++ 和线程非常陌生,所以如果您能提供一些伪代码解决方案来开始我的重新实现,我将不胜感激。

非常感谢

【问题讨论】:

  • 嗯,正如你所说,你可以使用互斥锁!所以每次创建线程时,锁定互斥锁,增加 ThreadCounter。然后解锁互斥锁。仅当计数器小于最大数时才创建互斥锁...
  • 你的跳帧策略是什么?如果你只是盲目地开始处理进来的前几帧,你最终会陷入一种只覆盖每秒前 16 帧的节奏,而不是每秒均匀采样。
  • 您输入的帧是实时的,还是从文件中解码的?如果它们是实时的,那么您确实会落后于两秒钟的计算,并且需要一些方法来应对。如果它们来自文件,您可以在队列已满时停止读取,并等到您赶上。
  • @Useless 它们是以 30 fps 的速度实时传入的帧,但我们确实不会处理所有这些帧,我们的想法是获得大约 5-6 fps。感谢您的回答
  • 好的,所以您预计需要大约 10-12 个线程来每秒产生 5-6 帧。你有那么多免费核心吗?

标签: c++ multithreading pool


【解决方案1】:

在这种情况下,我会避免使用线程池。来自wikipedia(强调我的):

[线程池] 提高了性能并避免了由于短期任务频繁创建和销毁线程而导致的执行延迟。

您的长时间运行的计算使创建和销毁线程所需的时间相形见绌,因此为每个任务创建一个线程对我来说似乎是合理的。你越能避免互斥体和合作,越好。至于一次运行很多线程,线程之间切换所花费的时间也与计算时间相形见绌,因此限制使用的线程数只会给你带来非常小的加速1

如果您的机器无法以足够快的速度完成计算以跟上传入的数据,您可能会遇到问题。如果您的所有 CPU 内核都以 100% 运行,那么您唯一能做的就是提高计算效率(可能对视频帧进行下采样?)或获得更多计算能力。


它们是以 30fps 的速度实时传入的帧。

1 我应该注意,对于实时应用程序,您应该将使用的线程数限制为内核数(或者更高一两个,对其进行分析) .这将减少接收帧和产生结果之间的延迟,而不会影响整体性能。

【讨论】:

  • 取决于您所说的“任务”。如果处理一对帧是一项“任务”,那么它的生命周期相对较短。
  • 我要离开“每次计算大约需要 2 秒”,这意味着处理一对帧。与stackoverflow.com/a/27764581/7619380 相比,线程创建时间约为 10 微秒。
  • 我的问题是,我已经为许多不同的系统编写了代码——从大型数据库服务器到以 sub-MHz 时钟速率运行的微控制器——我不再将任何特定的数值与想法联系起来比如短/长,快/慢,大/小。我为这些概念赋予抽象意义。当我听到“短期任务”时,我理解它的意思是“做一件事,然后结束”。当我听到“长期运行的线程”时,我想,“做的事情不定数量,......”但是,是的!你是对的。如果程序每帧使用 2 秒的 CPU,那么它就不会实时运行。
  • 谢谢@JoshWilson 我们将使用 16RAM 的机器。提高计算效率的一种可能方法是在 GPU 而不是 CPU 中计算运动估计,因为每次计算所需的时间不到 0.2 秒。正如我所说,我正在使用有限的资源。但是感谢您的方法,我认为使用线程池肯定会比我当前的实现更好
【解决方案2】:

粗略的可行性估计

...每次计算大约需要 2 秒。

...同时运行 4 或 8 个线程...

...大约 5-6 帧/秒

这些限制显然不起作用。

每秒产生 0.5 帧的 8 个线程最多可以为您提供每秒 4 帧。

如果您需要每秒 6 帧,则需要 12 个线程。此外,这些线程实际上需要绑定到真正的硬件内核。

接下来,您需要描述您的硬件平台。如果它没有至少有 12 个内核,你就不能按照你的要求去做,至少按照你建议的方式。

如果它有 12 个超线程“核心”,那也可能不够:一个线程可能会使所有 ALU 饱和。你还没有说你的框架有多大,但是 L1 压力也可能是一个问题。

如果您没有那么多内核,您要么需要更快地计算每一帧,要么牺牲每秒输出帧数。

实施

您说过要估计两个连续帧之间的运动。这意味着连续的输入帧,还是连续的输出帧?

第一种情况意味着您正在对输入进行采样,为每个输出读取两个 帧,这是更多数据,但您的线程可以并行进行:

Out0 = ME(In0, In1)

Out1 = ME(In6, In7)

(或ME(0,6), ME(6,12), ... 之类的)。

第二种情况意味着每个输出只需要 一个 输入帧,但是在第一个输出帧完成之前,您不能开始第二个输出帧(您正在比较第 n 个输入帧的第一个输出):

输出0 = 输入0

Out1 = ME(Out0, In6)

Out2 = ME(Out1, In12)


tl;dr 在真正开始编写任何代码之前,您需要澄清一些基本的事情。

【讨论】:

    【解决方案3】:

    你应该看看Intel TBB Task Scheduler。您可以将每个计算设为一个任务(具有execute 函数的派生类),并让调度程序为您在可用的CPU 上调度它。

    任务相对于逻辑线程的主要优势在于任务比逻辑线程轻得多。在 Linux 系统上,启动和终止任务比启动和终止线程快大约 18 倍。在 Windows 系统上,该比率超过 100。这是因为一个线程拥有自己的大量资源副本,例如寄存器状态和堆栈。在 Linux 上,线程甚至有自己的进程 ID。相比之下,英特尔® 线程构建模块中的任务通常是一个小例程,并且不能在任务级别被抢占(尽管它的逻辑线程可以被抢占)。

    调度程序执行负载平衡。除了使用正确数量的线程之外,在这些线程之间平均分配工作也很重要。只要您将程序分解为足够多的小任务,调度程序通常会很好地将任务分配给线程以平衡负载。使用基于线程的编程时,您经常会被困在自己处理负载平衡的问题上,这可能很难做到。

    最后,使用任务而不是线程的主要优点是它们可以让您在更高的、基于任务的层面上思考。使用基于线程的编程,您不得不考虑低级别的物理线程以获得良好的效率,因为每个物理线程都有一个逻辑线程,以避免订阅不足或过度订阅。您还必须处理相对粗粒度的线程。有了任务,你可以专注于任务之间的逻辑依赖,把高效的调度留给调度器。


    或者,如果您不能使用该库,您可以根据这些想法实现自己的任务调度程序。一个简单的实现将是一个多生产者多消费者队列,由池中固定数量的长寿命线程提供服务(对于计算线程池,您不希望线程数超过可用 CPU 内核的数量)。一个空闲线程会在队列中等待,当一个任务可用时抓取一个任务并执行它。

    【讨论】:

    • 您确实需要比内核更多的线程。特别是,您希望数字是可调的,以便您可以对其进行试验。即使线程纯粹受 CPU 限制,N+1 也是标准的。如果有任何阻塞 I/O,您肯定需要比内核更多的线程。
    猜你喜欢
    • 2012-01-25
    • 2020-04-24
    • 1970-01-01
    • 2013-03-23
    • 2019-07-15
    • 1970-01-01
    • 2020-08-19
    • 2010-11-27
    • 1970-01-01
    相关资源
    最近更新 更多