等待线程如何影响性能?

时间:2013-09-05 16:33:50

标签: c++ multithreading boost

我对多线程和线程一般都很新;请与我一起讨论这个问题,因为我不仅难以理解编程方面的内容,更重要的是当涉及到计算机正在做什么以及多线程如何影响性能时。我广泛搜索在线找到这个问题的答案,但无济于事。

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

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

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

- 我正在使用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;
                    }
                }
            }
        }
    }
}

提供的源代码严格来说明算法的复杂性。

1 个答案:

答案 0 :(得分:9)

如果线程真正等待,它们将不会消耗太多资源 - 只需要一点内存,并且在调度程序的等待列表中有一些“空格”插槽(因此会有少量的“唤醒”或“等待”一个线程的额外开销,因为还有一些数据需要处理 - 但这些队列通常效率很高,所以我怀疑你能否在实际线程所做的应用程序中测量它一些有意义的工作)。

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

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

根据相关编辑

修改

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

拥有更多线程而不是内核数量甚至可能导致处理为SLOWER,如果有更多线程准备运行而不是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.5s的CPU时间(或每个CPU大约0.4s)。当然,它只有5%左右,但浪费了5%的浪费。在很多情况下,算法的5%改进“值得拥有”。

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