防止线程不必要退出并保持线程池活跃。

3
假设我用 OMP_NUM_THREADS=16 调用一个程序。
第一个函数调用 #pragma omp parallel for num_threads(16)
第二个函数调用 #pragma omp parallel for num_threads(2)
第三个函数调用 #pragma omp parallel for num_threads(16)
使用 gdb 进行调试显示,在第二次调用时有 14 个线程退出。在第三次调用时,又会产生 14 个新的线程。
是否可以防止第二次调用时有 14 个线程退出呢?谢谢。
以下是证明清单。
$ cat a.cpp
#include <omp.h>

void func(int thr) {
    int count = 0;

    #pragma omp parallel for num_threads(thr)
    for(int i = 0; i < 10000000; ++i) {
        count += i;
    }        
}    

int main() {
    func(16);

    func(2);

    func(16);

    return 0;
} 

$ g++ -o a a.cpp -fopenmp -g

$ ldd a
...
libgomp.so.1 => ... gcc-9.3.0/lib64/libgomp.so.1 
...

$ OMP_NUM_THREADS=16 gdb a

...

Breakpoint 1, main () at a.cpp:13
13          func(16);
(gdb) n
[New Thread 0xffffbe24f160 (LWP 27216)]
[New Thread 0xffffbda3f160 (LWP 27217)]
[New Thread 0xffffbd22f160 (LWP 27218)]
[New Thread 0xffffbca1f160 (LWP 27219)]
[New Thread 0xffffbc20f160 (LWP 27220)]
[New Thread 0xffffbb9ff160 (LWP 27221)]
[New Thread 0xffffbb1ef160 (LWP 27222)]
[New Thread 0xffffba9df160 (LWP 27223)]
[New Thread 0xffffba1cf160 (LWP 27224)]
[New Thread 0xffffb99bf160 (LWP 27225)]
[New Thread 0xffffb91af160 (LWP 27226)]
[New Thread 0xffffb899f160 (LWP 27227)]
[New Thread 0xffffb818f160 (LWP 27228)]
[New Thread 0xffffb797f160 (LWP 27229)]
[New Thread 0xffffb716f160 (LWP 27230)]
15          func(2);
(gdb) 
[Thread 0xffffba9df160 (LWP 27223) exited]
[Thread 0xffffb716f160 (LWP 27230) exited]
[Thread 0xffffbca1f160 (LWP 27219) exited]
[Thread 0xffffb797f160 (LWP 27229) exited]
[Thread 0xffffb818f160 (LWP 27228) exited]
[Thread 0xffffbd22f160 (LWP 27218) exited]
[Thread 0xffffb899f160 (LWP 27227) exited]
[Thread 0xffffbda3f160 (LWP 27217) exited]
[Thread 0xffffbb1ef160 (LWP 27222) exited]
[Thread 0xffffb91af160 (LWP 27226) exited]
[Thread 0xffffba1cf160 (LWP 27224) exited]
[Thread 0xffffb99bf160 (LWP 27225) exited]
[Thread 0xffffbb9ff160 (LWP 27221) exited]
[Thread 0xffffbc20f160 (LWP 27220) exited]
17          func(16);
(gdb) 
[New Thread 0xffffbb9ff160 (LWP 27231)]
[New Thread 0xffffbc20f160 (LWP 27232)]
[New Thread 0xffffb99bf160 (LWP 27233)]
[New Thread 0xffffba1cf160 (LWP 27234)]
[New Thread 0xffffbda3f160 (LWP 27235)]
[New Thread 0xffffbd22f160 (LWP 27236)]
[New Thread 0xffffbca1f160 (LWP 27237)]
[New Thread 0xffffbb1ef160 (LWP 27238)]
[New Thread 0xffffba9df160 (LWP 27239)]
[New Thread 0xffffb91af160 (LWP 27240)]
[New Thread 0xffffb899f160 (LWP 27241)]
[New Thread 0xffffb818f160 (LWP 27242)]
[New Thread 0xffffb797f160 (LWP 27243)]
[New Thread 0xffffb716f160 (LWP 27244)]
19          return 0;
2个回答

2
简单来说,使用GCC无法强制运行时保留线程。从对libgomp源代码的初步阅读中,没有ICVs(可移植或供应商特定),可以防止在连续区域中终止多余的空闲线程。(如果我错了,请有人纠正我)。
如果您确实需要依赖于OpenMP运行时在不同团队大小的区域之间使用持久线程的不可移植要求,则请改用Clang或Intel C++而不是GCC。 Clang的(实际上是LLVM的)OpenMP运行时基于Intel的开源版本,它们都表现出您想要的方式。同样,这是不可移植的,并且行为可能会在将来的版本中更改。因此,建议不要以OpenMP实现的特定性能为前提编写代码。例如,如果循环所需的时间比创建线程团队(现代系统上为数十微秒的数量级)长几个数量级,则无论运行时是否使用持久线程都不会真正有所影响。
如果OpenMP开销确实是一个问题,例如,如果在循环中完成的工作量不足以摊销开销,则可移植的解决方案是提升并行区域,然后重新实现for工作共享结构,就像@dreamcrash的答案一样,或者(滥用)OpenMP的循环调度,通过设置仅导致所需数量的线程处理问题的块大小:
#include <omp.h>

void func(int thr) {
    static int count;
    const int N = 10000000;

    int rem = N % thr;
    int chunk_size = N / thr;

    #pragma omp single
    count = 0;

    #pragma omp for schedule(static,chunk_size) reduction(+:count)
    for(int i = 0; i < N-rem; ++i) {
        count += i;
    }

    if (rem > 0) {
        #pragma omp for schedule(static,1) reduction(+:count)
        for(int i = N-rem; i < N; ++i) {
            count += i;
        }
    }

    #pragma omp barrier
}

int main() {
    int nthreads = max of {16, 2, other values of thr};

    #pragma omp parallel num_threads(nthreads)
    {
        func(16);

        func(2);

        func(16);
    }

    return 0;
}

在所有线程中,您需要具有完全相等大小的块。第二个循环是为了处理thr不能整除迭代次数的情况。此外,不能简单地跨私有变量求和,因此必须共享count,例如通过将其设置为static。这很丑陋,并带来了一堆同步需求,可能具有与生成新线程相当的开销,并使整个练习毫无意义。


谢谢你的回答!我最近更深入地研究了这个问题。我尝试了两种变体:1)使用clang libomp和2)使用num_threads(max_thrs)以及正确数量的块(nthr = 2或nthr = 16),即像你和@dreamcrash的代码一样。有趣的是,这两种变体与我的初始版本相比表现更差(我的意思是迭代次数等于nthr)。 - JenyaKh
我指的是使用gcc libgomp的#pragma parallel for num_threads(nthr)for(int i = 0; i < nthr; i ++)比使用clang libomp的#pragma parallel for num_threads(nthr)和使用gcc libgomp的#pragma parallel for num_threads(MAX_THRS)都表现更好。 - JenyaKh
顺便提一下,在两种变体(使用clang-libomp和num_threads(MAX_THRS)+gcc-ligomp)中,我用gdb检查了线程确实只在程序的最后被杀死。你知道这是为什么吗?@dreamcrash - JenyaKh
对不起,我的意思是,在我上面的注释中的所有 #pragmas 中,循环是 for(int i=0;i < nthr; i++)。我的意思是,我将我的循环更改为始终具有适当数量的迭代次数(等于我想要的线程数--nthr)。 - JenyaKh
我的意思是,我不问为什么线程在程序结束时被终止 - 对于这两个变体来说,这是预期的。但为什么性能会下降3-4倍。 - JenyaKh
显示剩余7条评论

1
一种方法是创建一个单一的“并行区域”,过滤掉将执行“for”的线程,并手动按线程分配循环迭代。为简单起见,我将假设使用“parallel for schedule(static, 1)”:
include <omp.h>

void func(int total_threads) {
    int count = 0;
    int thread_id = omp_get_thread_num();
    if (thread_id < total_threads)
    {
       for(int i = thread_id; i < 10000000; i += total_threads) {
           count += i;
    }
    #pragma omp barrier          
}    

int main() {
    ...
    #pragma omp parallel num_threads(max_threads_to_be_used)
    {
        func(16);
        func(2);
        func(16);
    }
    return 0;
} 

请记住,存在一个竞争条件 count += i; 需要解决。在原始代码中,您可以通过使用 reduction 子句来轻松解决它,即 #pragma omp parallel for num_threads(thr) reduction(sum:count)。在手动 for 的代码中,您可以按以下方式解决它:
#include <omp.h>
#include<stdio.h>
#include <stdlib.h>

int func(int total_threads) {
    int count = 0;
    int thread_id = omp_get_thread_num();
    if (thread_id < total_threads)
    {
       for(int i = thread_id; i < 10000000; i += total_threads) 
           count += i;
    }
    return count;        
}    

int main() {
    int max_threads_to_be_used = // the max that you want;
    int* count_array = malloc(max_threads_to_be_used * sizeof(int));
    #pragma omp parallel num_threads(max_threads_to_be_used)
    {
        int count = func(16);
        count += func(2);
        count += func(16);
        count_array[omp_get_thread_num()] = count;
    }
    int count = 0;
    for(int i = 0; i < max_threads_to_be_used; i++) 
        count += count_array[i];
    printf("Count = %d\n", count);
    return 0;
} 

我认为大多数情况下,在每个并行区域中使用的线程数量是相同的。因此,这种类型的模式不应该是一个常见的问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接