为啥单个线程比多个线程快,即使它们本质上具有相同的开销?

Posted

技术标签:

【中文标题】为啥单个线程比多个线程快,即使它们本质上具有相同的开销?【英文标题】:Why is a single thread faster than multiple threads even though they essentially have the same overhead?为什么单个线程比多个线程快,即使它们本质上具有相同的开销? 【发布时间】:2013-03-28 03:03:34 【问题描述】:

我在 8 核处理器上运行 64 位 Windows 7。我运行了以下内容:

    #include "stdafx.h"
    #include <iostream>
    #include <Windows.h>
    #include <process.h>
    #include <ctime>

    using namespace std;

    int count = 0;
    int t = time(NULL);

    //poop() loops incrementing count until it is 300 million.
    void poop(void* params) 
        while(count < 300000000) 
            count++;
        


        cout<< time(NULL) - t <<" \n";
    

    int _tmain(int argc, _TCHAR* argv[])
    
        //_beginthread(poop, 0, NULL);      
        //_beginthread(poop, 0, NULL);
        poop(NULL);

        cout<<"done"<<endl;

        while(1);

        return 0;
    

我将结果与取消注释 beginThread 时的结果进行了比较。事实证明,单线程版本最快完成了这个!实际上,添加更多线程会使该过程花费更长的时间。计数 3 亿使该过程花费了 8 多秒,我认为这足以排除对 beginThread 的函数调用和其他小开销。

我做了一些研究,多线程进程变慢的一般结论是开销。但在这种情况下,无论我运行多个线程还是单个线程,变量计数(存在于数据段中,因为它是一个预先分配的变量 afaik)被访问的次数是相等的。所以基本上,开销(如果是开销问题)并不是因为访问全局变量比访问局部变量的成本更高。

查看我的任务管理器,单线程进程使用 13% 的 cpu(大约 1/8 个内核),添加线程会以大约 1/8 的增量增加 cpu 使用率。所以就cpu功率而言,假设任务管理器准确地描述了这一点,添加线程会使用更多的cpu。这进一步让我感到困惑.. 我如何使用更多的整体 cpu,具有单独的内核,但总体上需要更长的时间来完成任务?

TLDR:为什么会发生这种情况

【问题讨论】:

这看起来像是多个线程同时修改一个变量的雷区。 是的。缓存行争用。 多个线程在不同步的情况下修改同一个对象:未定义的行为。 【参考方案1】:

您的代码本质上是错误的。

count++ 是一个三步操作,它读取值、递增值,然后将其存储回变量中。 如果两个线程同时在同一个变量上运行count++,其中一个将覆盖另一个的更改。

因此,多线程版本最终会做额外的工作,因为每个线程都会破坏其他线程的进度。

如果您将count 设为局部变量,则时间应该看起来更正常。

或者,您可以使用互锁增量,这是线程安全的,但需要额外开销来跨线程同步。

【讨论】:

啊,谢谢你聪明的先生。你会说锁定增量和使用多线程比单线程快吗? @lululoo:不,因为一次只有一个线程会更新count。这就是同步的全部意义所在。递增整数需要加载、递增和存储。它不是原子操作。这不是多线程解决方案的好选择。您应该考虑一个更现实的问题,即可以分解为离散和独立任务的任务。【参考方案2】:

正如您最初问题的一些评论者指出的那样,您存在正确性和性能问题。首先,您的所有线程都在同时访问 count。这意味着无法保证线程实际上将全部计数到 3 亿。您可以通过在 poop 函数中声明 count 来解决此正确性错误

void poop(void* params) 
    int count  = 0;
    while(count < 300000000) 
        count++;
    
    cout<< time(NULL) - t <<" \n";

请注意,这对 t 来说不是问题,因为它只能由线程读取而不是写入。但是,cout 存在问题,因为您也是从多个线程写入的。

此外,正如 cmets 中所指出的,您的所有线程都在访问一个内存位置。这意味着当线程更新 count 时,必须刷新并重新加载保存它的缓存行。这是非常低效的内存访问。通常,当您访问数组中的连续元素而不是单个变量时会发生这种情况(坏主意,见上文)。对此的解决方案是填充您的阵列以确保每个条目都是您的 L1 缓存行大小的精确倍数,这显然是特定于您的目标处理器的。另一种选择是重组您的算法,以便:每个线程处理一个大块的连续元素,或者每个线程访问元素的方式使得线程不访问相邻的位置。

当您使用 Windows 时,您可能需要考虑为您的代码使用更高级别的抽象,而不是 Win32 线程函数。 Parallel Patterns Library 符合此处的要求(Intel's Threaded Building Blocks 也是如此)。

    concurrency::parallel_invoke(
        [=]  poop(nullptr); ,
        [=]  poop(nullptr); 
    );

这允许 PPL 将您的任务安排在线程池上,而不是您的应用程序必须显式创建线程。

您可能还认为,对于非常小的任务,启动额外线程的开销可能超过并行运行的收益。

【讨论】:

以上是关于为啥单个线程比多个线程快,即使它们本质上具有相同的开销?的主要内容,如果未能解决你的问题,请参考以下文章

操作系统线程

是否可以鼓励线程排队而不强迫它?

比Redis快5倍的中间件,为啥这么快?

多线程 ByteBuffers 比顺序慢?

Julia:即使被要求运行多个进程,spawnat 也始终在相同的线程上运行

具有多个逻辑数据流的单套接字连接(区分数据包)