【问题标题】:How do waiting threads affect performance?等待线程如何影响性能?
【发布时间】:2019-07-23 15:48:40
【问题描述】:

我正在编写一个程序,当遇到最坏的情况时,它的计算成本相对较高。我尝试动态创建线程,这已被证明在大多数时间都可以工作,但是当最坏的情况出现时,执行速度超出了我分配给完成这些计算的时间,这很大程度上是由于这些线程的创建和销毁。这让我想到了我过去使用的想法,即在执行之前创建线程,而不是动态地创建和销毁它们,并让它们在执行计算之前等待一个条件而不是动态地创建它们。

通常我不会三思而后行,但因为我将在系统初始化时创建大量线程,所以我担心这将如何影响系统的性能。这就引出了一个问题:等待条件的线程如何影响系统,如果有的话?是在程序初始化期间创建线程,并且仅在我需要执行计算时才通知它们是解决此问题的正确方法,还是存在我不知道的更好的解决方案?我也考虑过使用线程池来做到这一点。线程池是否最适合这种情况?

您可能会发现有助于更好地回答此问题的一些信息:

--我正在使用 boost 库(版本 1_54_0)来对程序进行多线程处理。

--我使用的是 Windows 7 和 Visual Studio。

--如果我在程序初始化时创建线程,我将创建 200-1000 个线程(这个数字是预先确定为 #define 的,我不必每次需要进行计算时都使用所有线程)。

--每次我需要执行此计算时,所需的线程数都会有所不同;它取决于每次执行计算时收到的输入数量,但不能超过最大值(最大数量在编译时确定为#define)。

--我用的电脑是32核的。

如果这个问题没有达到标准,我很抱歉;我是一个新的堆栈溢出用户,所以请随时询问更多信息并批评我如何更好地解释情况和问题。提前感谢您的帮助!

更新

这里是源代码(部分变量已根据我公司的条款和条件进行了重命名)

for(int i = curBlob.boundingBoxStartY; i < curBlob.boundingBoxStartY + curBlob.boundingBoxHeight; ++i)
{
    for(int j = curBlob.boundingBoxStartX; j < curBlob.boundingBoxStartX + curBlob.boundingBoxWidth; ++j)
    {
        for(int k = 0; k < NUM_FILTERS; ++k)
        {
            if((int)arrayOfBinaryValues[channel][k].at<uchar>(i,j) == 1)
            {
                for(int p = 0; p < NUM_FILTERS; ++p)
                {
                    if(p != k)
                    {
                        if((curBlob.boundingBoxStartX + 1 < (curBlob.boundingBoxStartX + curBlob.boundingBoxWidth)) && ((int)arrayOfBinaryValues[channel][k].at<uchar>(i + 1,j) == 1))
                            ++count;

                        if((curBlob.boundingBoxStartY + 1 < (curBlob.boundingBoxStartY + curBlob.boundingBoxHeight)) && ((int)arrayOfBinaryValues[channel][k].at<uchar>(i,j + 1) == 1))
                            ++count;
                    }
                }
            }
        }
    }
}

提供的源代码是为了展示算法的复杂性。

【问题讨论】:

  • 我怀疑您甚至“需要” 200-1000 个线程。当您达到 4 位数时,上下文切换的绝对水平将是可怕的。您可能需要考虑工作人员通过队列分派工作中的线程池。它消除了队列的启动/关闭时间,并且很可能会更好地并行化,特别是如果您的池是基于核心计数因素配置的。
  • @WhozCraig 200 到 1000 个线程对系统性能几乎没有影响,因为它们中的大多数将处于等待状态。现在我的机器有大约 500 个活动线程,CPU 基本上是 0%(不包括我看的时候似乎吃掉 CPU 的 Iceweasel)。但是,我完全同意这么多线程很可能毫无用处,而工作线程池会更合适。
  • @syam 我不是在谈论他们空闲的时候;我说的是他们嘎吱嘎吱的时候。我的窗外是一个停车场,停满了没有做任何事情的汽车,但这并不意味着在几个小时内他们都不会陷入可怕的交通拥堵中试图回家(并进入每个其他的方式)。我认为我们都同意 OP 应该考虑除了每个活动一个线程方法之外的替代方案。
  • 感谢克雷格的洞察力。很好的比喻:)
  • 即使使用 CPU 密集型线程,一旦就绪线程数超过内核数的两倍,与上下文切换相关的开销就会变平。在那之后,每秒中断的数量不会随着线程数量而神奇地增加,并且只有在中断时操作系统才能进行上下文切换。

标签: c++ multithreading boost


【解决方案1】:

如果线程真的在等待,它们根本不会消耗太多资源 - 只是一点内存,以及调度程序等待列表中的一些“空间”槽(因此会有少量“唤醒”或“等待”线程的额外开销,因为要处理更多数据 - 但这些队列通常相当有效,所以我怀疑你是否能够在实际线程执行的应用程序中测量它一些有意义的工作)。

当然,如果它们定期唤醒,即使每秒唤醒一次,1000 个线程每秒唤醒一次也意味着每毫秒切换一次上下文,这可能会影响性能。

但我确实认为在几乎所有情况下创建许多线程都是错误的解决方案。除非线程中的逻辑很复杂,并且每个线程都有大量的状态/上下文要跟踪,并且这种状态或上下文不容易存储在某个地方,否则这样做可能是正确的。但在大多数情况下,我会说使用少量工作线程,然后拥有一个工作项队列(包括[某种类型的引用]它们各自的状态或上下文)将是实现这一目标的更好方法。

编辑基于有问题的编辑:

由于(据我所知)线程完全受 CPU(或内存带宽)约束 - 没有 I/O 或其他“等待”,因此每个内核运行一个线程可以实现最大性能在系统中(对于“其他需要做的事情,例如通过网络、磁盘 I/O 和需要完成的一般操作系统/系统工作进行通信,可能是“减一”)。

线程数多于内核数甚至可能导致处理速度变慢,如果准备运行的线程数多于 CPU 上的内核数,因为现在操作系统将有多个线程“争夺”时间,这将在操作系统方面造成额外的线程调度工作,最重要的是,当一个线程运行时,它会加载有用内容的缓存。当另一个线程在同一个 CPU 内核上运行时,它将强制缓存将其他数据加载到缓存中,当“旧”线程再次运行时,即使它在同一个 CPU 上,它也必须重新加载它使用的数据。

我将做一个快速实验,然后为我的一个项目返回一些数字...

所以,我有一个计算“weird numbers”的小项目。我在这里将其用作“比较运行一个线程与更多线程所需的时间”。这里的每个线程都使用相当少的内存——几百字节,所以缓存可能根本没有效果。所以这里唯一的变量是“启动成本”和由于线程之间的竞争而导致的边际开销。线程数由-t 选项决定。 -e 是“停在什么号码”。

$ time ./weird -t 1 -e 50000 > /dev/null

real    0m6.393s
user    0m6.359s
sys 0m0.003s
$ time ./weird -t 2 -e 50000 > /dev/null

real    0m3.210s
user    0m6.376s
sys 0m0.013s
$ time ./weird -t 4 -e 50000 > /dev/null

real    0m1.643s
user    0m6.397s
sys 0m0.024s
$ time ./weird -t 8 -e 50000 > /dev/null

real    0m1.641s
user    0m6.397s
sys 0m0.028s
$ time ./weird -t 16 -e 50000 > /dev/null

real    0m1.644s
user    0m6.385s
sys 0m0.047s
$ time ./weird -t 256 -e 50000 > /dev/null

real    0m1.790s
user    0m6.420s
sys 0m0.342s
$ time ./weird -t 512 -e 50000 > /dev/null

real    0m1.779s
user    0m6.439s
sys 0m0.502s

如您所见,“运行”整个项目的时间从 1 个增加到 2 个,从 2 个线程增加到 4 个。但是运行超过 4 个线程会得到几乎相同的结果,直到我们达到数百个(我跳过了几个步骤来将线程数加倍)。

现在,为了显示调度开销,我在-e 之后用更大的数字增加了“要查找的数字”的数量(这也使进程运行时间更长,因为更大的数字计算起来更复杂)。

$ time ./weird -t 512 -e 100000 > /dev/null

real    0m7.100s
user    0m26.195s
sys 0m1.542s
$ time ./weird -t 4 -e 100000 > /dev/null

real    0m6.663s
user    0m26.143s
sys 0m0.049s

现在,如果仅花费启动时间,我们应该会看到 512 个线程达到 50000 和 512 个线程达到 100000 之间的类似开销(sys),但我们看到的数字要高出三倍.因此,在 6-7 秒内,运行 512 个线程(全速)与运行 4 个线程相比浪费了近 1.5 秒的 CPU 时间(或每个 CPU 大约 0.4 秒)。当然,这只是大约 5%,但 5% 的浪费努力仍然是浪费的。在很多情况下,算法 5% 的改进是“值得拥有的”。

是的,这是一个极端情况,可以说只要大多数线程都在等待,这并不重要。

【讨论】:

  • 创建少量工作线程然后拥有工作项队列的方法本质上是线程池吗?
  • 是的,这是同一事物的另一个名称(或者至少,它总体上等同于同一事物 - 我确信“线程池”非常具体)。
  • 我将尝试这种方法,并让您知道它是如何进行的。可能需要一段时间,因为我对多线程仍然很陌生,并且以前从未实现过线程池。不过感谢您的帮助,非常感谢! :)
  • 我必须承认,对于这种特殊情况,我只做了一个非常简单的方法——我已经在单核 286 或 386 上使用了几十个线程,每个线程都在控制它自己的一块的硬件。那样工作得很好。但是对于大多数问题来说,几十个线程似乎是错误的解决方案——除非你有一些非常奇怪的硬件,那里有成百上千的硬件实例需要照顾。
  • 根据我的多线程经验,我从来没有做过多于几个并行工作的线程(最多我会说在 5 到 10 个之间)。我试图执行的计算非常激烈。收到输入时,我收到的每个项目大约需要 15 毫秒。我觉得我迷路的地方是我不明白为什么创建 200 个线程并在需要执行计算时只通知我需要的线程不是正确的解决方案,或者说是最佳解决方案,尤其是当我平均每次收到输入时只会通知 20 个线程。
猜你喜欢
  • 2018-09-29
  • 2023-03-30
  • 1970-01-01
  • 1970-01-01
  • 2021-06-23
  • 1970-01-01
  • 2017-01-25
  • 1970-01-01
  • 2020-04-19
相关资源
最近更新 更多