防止线程不必要的退出并使池保持活动状态

Posted

技术标签:

【中文标题】防止线程不必要的退出并使池保持活动状态【英文标题】:Preventing threads from unnecessary exit and keep the pool alive 【发布时间】:2021-03-27 05:03:03 【问题描述】:

假设我用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;

【问题讨论】:

【参考方案1】:

简单的答案是 GCC 不可能强制运行时保留线程。通过粗略阅读source code of libgomp,没有可移植或特定于供应商的 ICV 可以防止终止连续区域中多余的空闲线程。 (如果我错了,有人纠正我)

如果您确实需要依赖 unportable 要求,即 OpenMP 运行时使用跨区域的持久线程,并且团队规模各不相同,那么请使用 Clang 或 Intel C++ 而不是 GCC。 Clang 的(实际上是 LLVM 的)OpenMP 运行时基于英特尔的开源版本,它们的行为都符合您的要求。同样,这不是可移植的,并且行为可能会在将来的版本中发生变化。相反,建议不要以这样一种方式编写代码,即其性能取决于 OpenMP 实现的特殊性。例如,如果循环比创建线程组花费的时间多几个数量级(在现代系统上大约为几十微秒),那么运行时是否使用持久线程并不重要。

如果 OpenMP 开销确实是个问题,例如,如果在循环中完成的工作不足以分摊开销,那么可移植的解决方案是提升并行区域,然后重新实现 for 工作共享结构,例如在@dreamcrash 的答案中或(ab)通过设置块大小来使用 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。这很丑陋,并且拖累了一堆同步必需品,这些同步必需品的开销可能与生成新线程相当,并使整个练习毫无意义。

【讨论】:

嗨 Histro,我认为你可以让 count 成为私有并且仍然可以减少,例如我所做的,当然最后它需要某种线程之间的共享结构跨度> 感谢您的回答!我最近更多地解决了这个问题。我尝试了两种变体:1)。使用clang libomp和2)。使用 num_threads(max_thrs) 和适当数量的块(nthr=2 或 nthr=16),即就像在您和 @dreamcrash 的代码中一样。有趣的是,与我的初始版本相比,这两种变体的性能都较差(我的意思是迭代次数等于 nthr) 我的意思是#pragma parallel for num_threads(nthr) for(int i=0;i 顺便说一句,在这两种变体中(使用 clang-libomp 和使用 num_threads(MAX_THRS)+gcc-ligomp)——我用 gdb 检查了线程确实只在最后才被杀死该程序。你知道为什么会这样吗? @dreamcrash 对不起,我的意思是,在上面我的 cmets 中的所有#pragmas 中,循环是 for(int i=0;i 【参考方案2】:

一种方法是创建一个parallel region,过滤掉将执行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;
 

我会说大多数时候,每个并行区域中使用的线程数相同。所以这种类型的模式不应该是一个经常发生的问题。

【讨论】:

以上是关于防止线程不必要的退出并使池保持活动状态的主要内容,如果未能解决你的问题,请参考以下文章

防止android在崩溃后重新创建活动堆栈

防止键盘在活动开始时显示

Flutter Desktop:如何隐藏窗口(并使进程保持活动状态)?

防止睡眠模式

如何防止在 Android 片段/活动中意外退出应用程序?

如何防止触摸设备上按钮的粘滞悬停效果