为什么多线程代码(使用 pthreads)看起来比多进程代码(使用 fork)慢?
Why does multi-threaded code (using pthreads) seem slower than multi-process code (using fork)?
在这里,我尝试使用 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 秒左右的执行时间是相似的。
输出:
TL;DR:你用错误的方式测量时间。使用 clock_gettime(CLOCK_MONOTONIC, ...)
而不是 clock()
.
您正在使用 clock()
测量时间,如手册页所述:
[...] returns an approximation of processor time used by the program. [...]
The value returned is the CPU time used so far as a clock_t
clock()
使用的系统时钟测量 CPU 时间,这是调用进程在使用 CPU 时花费的时间。进程使用的 CPU 时间是其所有线程使用的 CPU 时间的总和,但 而不是 它的 children,因为这些是不同的过程。另见:What specifically are wall-clock-time, user-cpu-time, and system-cpu-time in UNIX?
因此,在您的 3 个场景中会出现以下情况:
无并行,顺序代码。 CPU 花费在 运行 过程中的时间几乎是所有可以衡量的,并且与实际花费的 wall-clock 时间非常相似。请注意,单线程程序的 CPU 时间始终低于或等于其 wall-clock 时间。
多个child进程。由于您正在创建 child 进程来代表主要 (parent) 进程完成实际工作,因此 parent 将使用几乎零 CPU 时间:唯一它必须做的是创建 children 的几个系统调用,然后是等待它们退出的几个系统调用。它的大部分时间都花在等待 children 上,而不是 运行ning 上 CPU。 children 进程是 CPU 上 运行 的进程,但您根本没有测量它们的时间。因此,您最终的时间很短(1 毫秒)。你在这里基本上没有测量任何东西。
多线程。由于您正在创建 N 个线程来完成工作,并且仅在主线程中占用 CPU 时间,因此您进程的 CPU 时间将占 CPU 时间的总和线程。如果您进行完全相同的计算,那么每个线程花费的平均 CPU 时间是 T/NTHREADS,将它们相加将得到 T/NTHREADS * NTHREADS = T. 实际上,您使用的 CPU 时间与第一个场景大致相同,只是创建和管理线程的开销很小。
所有这些都可以通过两种方式解决:
- 在每个 thread/process 中以正确的方式仔细计算 CPU 时间,然后根据需要对值进行求和或平均。
- 使用
clock_gettime
和 CLOCK_REALTIME
、CLOCK_MONOTONIC
或 [=] 之一简单地测量 wall-clock 时间(即真实人类时间)而不是 CPU 时间18=]。有关详细信息,请参阅 the manual page。
在这里,我尝试使用 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 秒左右的执行时间是相似的。
输出:
TL;DR:你用错误的方式测量时间。使用 clock_gettime(CLOCK_MONOTONIC, ...)
而不是 clock()
.
您正在使用 clock()
测量时间,如手册页所述:
[...] returns an approximation of processor time used by the program. [...] The value returned is the CPU time used so far as a
clock_t
clock()
使用的系统时钟测量 CPU 时间,这是调用进程在使用 CPU 时花费的时间。进程使用的 CPU 时间是其所有线程使用的 CPU 时间的总和,但 而不是 它的 children,因为这些是不同的过程。另见:What specifically are wall-clock-time, user-cpu-time, and system-cpu-time in UNIX?
因此,在您的 3 个场景中会出现以下情况:
无并行,顺序代码。 CPU 花费在 运行 过程中的时间几乎是所有可以衡量的,并且与实际花费的 wall-clock 时间非常相似。请注意,单线程程序的 CPU 时间始终低于或等于其 wall-clock 时间。
多个child进程。由于您正在创建 child 进程来代表主要 (parent) 进程完成实际工作,因此 parent 将使用几乎零 CPU 时间:唯一它必须做的是创建 children 的几个系统调用,然后是等待它们退出的几个系统调用。它的大部分时间都花在等待 children 上,而不是 运行ning 上 CPU。 children 进程是 CPU 上 运行 的进程,但您根本没有测量它们的时间。因此,您最终的时间很短(1 毫秒)。你在这里基本上没有测量任何东西。
多线程。由于您正在创建 N 个线程来完成工作,并且仅在主线程中占用 CPU 时间,因此您进程的 CPU 时间将占 CPU 时间的总和线程。如果您进行完全相同的计算,那么每个线程花费的平均 CPU 时间是 T/NTHREADS,将它们相加将得到 T/NTHREADS * NTHREADS = T. 实际上,您使用的 CPU 时间与第一个场景大致相同,只是创建和管理线程的开销很小。
所有这些都可以通过两种方式解决:
- 在每个 thread/process 中以正确的方式仔细计算 CPU 时间,然后根据需要对值进行求和或平均。
- 使用
clock_gettime
和CLOCK_REALTIME
、CLOCK_MONOTONIC
或 [=] 之一简单地测量 wall-clock 时间(即真实人类时间)而不是 CPU 时间18=]。有关详细信息,请参阅 the manual page。