为啥多线程(使用 pthread)似乎比多进程(使用 fork)慢?

Posted

技术标签:

【中文标题】为啥多线程(使用 pthread)似乎比多进程(使用 fork)慢?【英文标题】:Why the multi-threading(using pthread) seems slower than multi-process(using fork)?为什么多线程(使用 pthread)似乎比多进程(使用 fork)慢? 【发布时间】:2021-12-14 01:49:18 【问题描述】:

这里我尝试使用 3 种方法将 0 到 1e9 之间的所有数字相加:

    正常顺序执行(单线程) 创建多个进程以添加较小的部分(使用 fork)并在最后添加所有较小的部分,并且 创建多个线程以执行与第二种方法相同的操作。

据我所知,线程创建速度很快,因此被称为轻量级进程。

但是在执行我的代码时,我发现第二种方法(多进程)最快,其次是第一种方法(顺序),然​​后是第三种方法(多线程)。但我无法弄清楚为什么会发生这种情况(可能是执行时间计算中的一些错误,或者是我的系统中有一些不同的东西等等)。

这是我的代码 C 代码:

#include "stdlib.h"
#include "stdio.h"
#include "unistd.h"
#include "string.h"
#include "time.h"
#include "sys/wait.h"
#include "sys/types.h"
#include "sys/sysinfo.h"
#include "pthread.h"
#define min(a,b) (a < b ? a : b)

int n = 1e9 + 24; // 2, 4, 8 multiple 

double show(clock_t s, clock_t e, int n, char *label)
    double t = (double)(e - s)/(double)(CLOCKS_PER_SEC);
    printf("=== N %d\tT %.6lf\tlabel\t%s === \n", n, t, label);
    return t;


void init()
    clock_t start, end;
    long long int sum = 0;
    start = clock();
    for(int i=0; i<n; i++) sum += i;
    end = clock();
    show(start, end, n, "Single thread");
    printf("Sum %lld\n", sum); 


long long eachPart(int a, int b)
    long long s = 0;
    for(int i=a; i<b; i++) s += i;
    return s;

// multiple process with fork
void splitter(int a, int b, int fd[2], int n_cores) // a,b are useless (ignore)
    clock_t s, e;
    s = clock();
    int ncores = n_cores;
    // printf("cores %d\n", ncores);
    int each = (b - a)/ncores, cc = 0;
    pid_t ff; 
    for(int i=0; i<n; i+=each)
        if((ff = fork()) == 0 )
            long long sum = eachPart(i, min(i + each, n) );
            // printf("%d->%d, %d - %d - %lld\n", i, i+each, cc, getpid(), sum);
            write(fd[1], &sum, sizeof(sum));
            exit(0);
        
        else if(ff > 0) cc++;
        else printf("fork error\n");
    
    int j = 0;
    while(j < cc)
        int res = wait(NULL);
        // printf("finished r: %d\n", res);
        j++;
    
    long long ans = 0, temp;
    while(cc--)
        read(fd[0], &temp, sizeof(temp));
        // printf("c : %d, t : %lld\n", cc, temp);
        ans += temp;
    
    e = clock();
    show(s, e, n, "Multiple processess used");
    printf("Sum %lld\tcores used %d\n", ans, ncores);



// multi threading used 
typedef struct SS
    int s, e;
 SS;

int tfd[2];

void* subTask(void *p)
    SS *t = (SS*)p;
    long long *s = (long long*)malloc(sizeof(long long)); 
    *s = 0;
    for(int i=t->s; i<t->e; i++)
        (*s) = (*s) + i;
    
    write(tfd[1], s, sizeof(long long));
    return NULL;


void threadSplitter(int a, int b, int n_thread) // a,b are useless (ignore)
    clock_t sc, e;
    sc = clock();
    int nthread = n_thread;
    pthread_t thread[nthread];
    int each = n/nthread, cc = 0, s = 0;
    for(int i=0; i<nthread; i++)
        if(i == nthread - 1)
            SS *t = (SS*)malloc(sizeof(SS));
            t->s = s, t->e = n; // start and end point
            if((pthread_create(&thread[i], NULL, &subTask, t))) printf("Thread failed\n");
            s = n; // update start point
        
        else 
            SS *t = (SS*)malloc(sizeof(SS));
            t->s = s, t->e = s + each; // start and end point
            if((pthread_create(&thread[i], NULL, &subTask, t))) printf("Thread failed\n");
            s += each; // update start point
        
    
    long long ans = 0, tmp;
    // for(int i=0; i<nthread; i++)
    //     void *dd;
    //     pthread_join(thread[i], &dd); 
    //     // printf("i : %d s : %lld\n", i, *((long long*)dd));
    //     ans += *((long long*)dd);
    // 
    int cnt = 0;
    while(cnt < nthread)
        read(tfd[0], &tmp, sizeof(tmp));
        ans += tmp;
        cnt += 1;
    
    e = clock();
    show(sc, e, n, "Multi Threading");
    printf("Sum %lld\tThreads used %d\n", ans, nthread);


int main(int argc, char* argv[])
    init();

    printf("argc : %d\n", argc);
    
    // ncore - processes
    int fds[2];
    pipe(fds);
    int cores = get_nprocs();
    splitter(0, n, fds, cores);
    for(int i=1; i<argc; i++)
        cores = atoi(argv[i]);
        splitter(0, n, fds, cores);
    
    
    // nthread - calc
    pipe(tfd); 
    threadSplitter(0, n, 16);
    for(int i=1; i<argc; i++)
        int threads = atoi(argv[i]);
        threadSplitter(0, n, threads);
    

    return 0;


输出结果:

=== N 1000000024    T 2.115850  label   Single thread === 
Sum 500000023500000276
argc : 4
=== N 1000000024    T 0.000467  label   Multiple processess used === 
Sum 500000023500000276  cores used 8
=== N 1000000024    T 0.000167  label   Multiple processess used === 
Sum 500000023500000276  cores used 2
=== N 1000000024    T 0.000436  label   Multiple processess used === 
Sum 500000023500000276  cores used 4
=== N 1000000024    T 0.000755  label   Multiple processess used === 
Sum 500000023500000276  cores used 6
=== N 1000000024    T 2.677858  label   Multi Threading === 
Sum 500000023500000276  Threads used 16
=== N 1000000024    T 2.204447  label   Multi Threading === 
Sum 500000023500000276  Threads used 2
=== N 1000000024    T 2.235777  label   Multi Threading === 
Sum 500000023500000276  Threads used 4
=== N 1000000024    T 2.534276  label   Multi Threading === 
Sum 500000023500000276  Threads used 6

另外,我使用管道来传输子任务的结果。在多线程中,我也尝试使用连接线程并顺序合并结果,但最终结果在 2 秒左右的执行时间左右。

输出:

【问题讨论】:

AFAIK, clock 不要衡量你认为它做了什么(即不是挂钟时间)。 Il 不会以相同的方式处理线程和进程。尝试使用另一种方式来测量时间,例如gettimeofday clock() 可能会赢得标准 C 库中最不幸命名函数的奖项:/ 【参考方案1】:

TL;DR:您以错误的方式测量时间。使用clock_gettime(CLOCK_MONOTONIC, ...) 而不是clock()


您正在使用clock() 测量时间,如手册页所述:

[...] 返回程序使用的处理器时间的近似值。 [...] 返回的值是到目前为止使用的 CPU 时间clock_t

clock() 使用的系统时钟测量 CPU 时间,即调用进程在使用 CPU 时所花费的时间。进程使用的 CPU 时间是其所有线程使用的 CPU 时间的总和,但不是它的子线程,因为它们是不同的进程。另见:What specifically are wall-clock-time, user-cpu-time, and system-cpu-time in UNIX?

因此,在您的 3 个场景中会发生以下情况:

    没有并行性,顺序代码。运行进程所花费的 CPU 时间几乎是所有需要衡量的,并且与实际花费的挂钟时间非常相似。请注意,单线程程序的 CPU 时间总是小于或等于其挂钟时间。

    多个子进程。由于您正在创建子进程来代表主(父)进程执行实际工作,因此父进程将使用几乎为零的 CPU 时间:它唯一要做的就是创建子进程的一些系统调用,然后是一些系统调用等待它们退出。它的大部分时间都花在等待孩子的睡眠上,而不是在 CPU 上运行。子进程是在 CPU 上运行的进程,但您根本没有测量它们的时间。因此,您最终会得到很短的时间(1ms)。你基本上没有在这里测量任何东西。

    多线程。由于您正在创建 N 个线程来完成工作,并且仅在主线程中占用 CPU 时间,因此您的进程的 CPU 时间将占线程 CPU 时间的总和。毫不奇怪,如果您进行完全相同的计算,每个线程花费的平均 CPU 时间为 T/NTHREADS,将它们相加将得出 T/NTHREADS * NTHREADS = T。事实上,您使用的大致是与第一种情况相同的 CPU 时间,只是创建和管理线程的开销很小。

所有这些都可以通过两种方式解决:

    在每个线程/进程中以正确的方式仔细计算 CPU 时间,然后根据需要对这些值进行求和或平均。 使用clock_gettimeCLOCK_REALTIMECLOCK_MONOTONICCLOCK_MONOTONIC_RAW 之一简单地测量挂钟时间(即真实人类时间)而不是CPU 时间。请参阅the manual page 了解更多信息。

【讨论】:

非常感谢。现在,结果相当合理,但对于较大的 n 值,多进程方法仍然稍微好一些(大约 0.1 秒)。有什么理由吗?或者是因为多个过程,准确的时间计算很困难。 @devi_D 我不确定可能是什么问题,如果我用clock_gettime(CLOCK_REALTIME, ...) 替换代码中的clock() 调用并调整show 函数以获取两个struct timespect 和正确打印时间我可以看到多个子线程比多个线程快 0.1 秒,这又比单线程快。如果线程的运行速度比多子场景慢,您可能在线程场景中开销过多(例如不需要管道)。 @devi_D 如果我使用-O3 编译,我可以看到多线程方案是最快的,因此可能是编译器在较低的优化级别上没有足够好地优化内存访问。

以上是关于为啥多线程(使用 pthread)似乎比多进程(使用 fork)慢?的主要内容,如果未能解决你的问题,请参考以下文章

Python36 1.joinablequeue 2.线程理论 3.多线程对比多进程 4.线程的使用方式 4.1.产生 线程的两种方式 4.2.守护线程 4.3.线程安全问题 4.3.1.互斥锁 4

在循环中保存图像比多线程/多处理更快

C/C++多线程

从 pthread 调用 sleep() 是不是会使线程进入睡眠状态或进程?

php Pthread 多线程 基本介绍

Linux_多线程(进程与线程的联系_pthread库_线程创建_线程等待_线程正常终止_线程取消_线程分离_pthread_t与LWP)