为什么 6-7 个线程比 20 个线程快?

Why is 6-7 threads faster than 20?

在学校里,我们学习了 C++11 线程。老师给了我们一个简单的评估,就是用20个线程做一个基本的网络爬虫。对我来说线程是很新的,虽然我确实了解基础知识。

我想提一下,我不是在找人来完成我的评估,因为它已经完成了。我只想了解为什么使用 6 个线程总是比使用 20 个线程快的原因。

请参阅下面的代码示例。

main.cpp:

do
{
    for (size_t i = 0; i < THREAD_COUNT; i++)
    {
        threads[i] = std::thread(SweepUrlList);
    }

    for (size_t i = 0; i < THREAD_COUNT; i++)
    {
        threads[i].join();
    }

    std::cout << std::endl;
    WriteToConsole();
    listUrl = listNewUrl;
    listNewUrl.clear();
} while (listUrl.size() != 0);

基本上这会为每个工作线程分配要完成的作业,这是可以在下面找到的方法 SweepUrlList 然后加入所有线程。

while (1)
{
    mutextGetNextUrl.lock();
    std::set<std::string>::iterator it = listUrl.begin();
    if (it == listUrl.end())
    {
        mutextGetNextUrl.unlock();
        break;
    }
    std::string url(*it);
    listUrl.erase(*it);
    mutextGetNextUrl.unlock();
    ExtractEmail(url, listEmail);
    std::cout << ".";
}

所以每个工作线程循环直到ListUrl为空。 ExtractEmail 是一种下载网页(使用 curl)并解析它以从 mailto 链接中提取电子邮件的方法。 ExtractEmail 中唯一的阻塞调用可以在下面找到:

if(email.length() != 0)
{
    mutextInsertNewEmail.lock();
    ListEmail.insert(email);
    mutextInsertNewEmail.unlock();
}

欢迎所有答案,如果可能,请链接到您找到的任何文档来回答这个问题。

这是一个相当普遍的线程问题,其核心是:

你演示的是线程Scheduling。操作系统将与各种线程一起工作,并在当前不工作的地方安排工作。

假设您有 4 个内核和超线程,您有 8 个处理器可以承载负载,但也可以承载其他应用程序(操作系统、C++ 调试器和要启动的应用程序)。

理论上,在大约 8 个密集线程之前,您的性能可能还不错。在达到处理器可以有效使用的最多线程数后,线程开始相互竞争资源。性能不佳可以看出这一点(尤其是密集型应用程序和紧密循环)。

最后,这是一个简化的答案,但我怀疑你看到的是什么。

简单的答案是瓶颈。你正在做的事情正在造成瓶颈。发生这种情况时会减速。它可能是您与某物建立的活动连接的数量,或者仅仅是线程数量和内存大小的额外开销(请参阅下面关于核心是这些扼流圈之一的答案)。

您将需要设置一系列监视器来调查您的瓶颈所在,以及需要更改哪些内容才能实现规模化。每个行业的许多系统每天都面临这个问题。在一端打开油门并不等于在另一端增加相同的输出。在某些情况下,它可以减少另一端的输出。

以个人离开大厅为例。目标是尽快让 100 人离开大楼。如果单个文件每 1 秒产生 1 人的速度,则需要 100 秒来清除建筑物。我们可以将他们并排派出 2 个,从而将时间减半,这样 50 秒就可以清理建筑物。如果我们然后将它们作为 8 个并排发送怎么办?门只有2m宽,并排8个相当于4m,第一排只有50%能通过。其他 4 个将导致下一行阻塞,依此类推。根据速率,这可能会导致暂时阻塞并增加 10 倍的时间。

线程是一种操作系统结构。基本上,每个线程的状态(基本上是 CPU 的所有寄存器和虚拟内存映射 [这是进程构造的一部分])由操作系统保存。一旦 OS 给出特定线程 "execution time",它就会恢复此状态并让它 运行。一旦这个时间结束,它必须保存这个状态。保存特定线程的状态并恢复另一个线程的过程称为 Context Switching,这需要大量时间(通常在数百到数千个 CPU 周期之间)。

上下文切换还有额外的惩罚。一些处理器的缓存(如虚拟内存翻译缓存,称为TLB)必须被刷新,流水线指令被丢弃等等。通常,您希望尽可能减少上下文切换。

如果您的 CPU 有 4 个核心,那么 4 个线程可以同时 运行。如果您尝试在 4 核系统上 运行 20 个线程,那么 OS 必须管理这些线程之间的时间,因此看起来它们 运行 是并行的。例如,线程 1-4 将 运行 持续 50 毫秒,然后线程 5-9 将 运行 持续 50 毫秒,等等

因此,如果您的所有线程都是 运行ning CPU 密集型操作,通常最有效的方法是让您的程序使用与内核相同数量的线程(有时称为 'processors' 在 windows 中)。如果您的线程多于内核,则必须进行上下文切换,并且可以将开销降至最低。

总的来说,并不是线程越多越好。更多线程以两种方式提供价值:更高的并行度和更少的阻塞。更高的内存、更高的上下文切换和更高的资源争用会损害更多线程。

更多线程对于更高并行度的价值通常在您可用的实际核心数的 1-2 倍之间最大化。如果您的线程已经 CPU 绑定,则最大值通常是核心数的 1 倍。

减少阻塞的价值更难量化,并且取决于您执行的工作类型。如果您受 IO 限制,并且您的线程主要是在等待 IO 就绪,那么更多的线程可能是有益的。

但是,如果您在线程之间共享状态,或者您正在线程之间进行某种形式的消息传递,那么您将 运行 陷入同步和争用问题。随着线程数量的增加,这些类型的开销和上下文切换越多,越多地占据了执行任务所花费的时间。

Amdahl's law 是一种有用的衡量标准,可用于确定更高的并行性是否真的会改善您的工作的总 运行时间。

您还必须注意,增加的并行度不会超过某些其他资源,例如总内存或磁盘或网络吞吐量。一旦使当前瓶颈饱和,您将不会通过增加线程数看到性能提升。

在进行任何性能调整之前,了解主要的资源瓶颈是什么很重要。有很多工具可以进行系统范围的资源监控。在 Linux 上,一个非常有用的工具是 dstat。在 Windows,您可以使用任务管理器来监控其中的许多资源。