Предотвращение ненужного выхода потоков и сохранение пула в рабочем состоянии

#c #c #multithreading #openmp #threadpool

#c #c #многопоточность #openmp #threadpool

Вопрос:

Предположим, я вызываю программу с 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 невозможно заставить среду выполнения поддерживать потоки. При беглом чтении исходного кода libgomp нет ICV, переносимых или зависящих от поставщика, которые предотвращают завершение избыточных незанятых потоков в последовательных регионах. (кто-нибудь, поправьте меня, если я ошибаюсь)

Если вам действительно нужно полагаться на непереносимое требование, чтобы среда выполнения OpenMP использовала постоянные потоки в регионах с разным размером команды между ними, тогда используйте Clang или Intel C вместо GCC. Среда выполнения Clang (фактически LLVM) OpenMP основана на версии Intel с открытым исходным кодом, и они оба ведут себя так, как вы хотите. Опять же, это не переносимо, и поведение может измениться в будущих версиях. Вместо этого рекомендуется не писать свой код таким образом, чтобы его производительность зависела от особенностей реализации 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 . Это некрасиво и влечет за собой множество потребностей в синхронизации, которые могут иметь накладные расходы, сопоставимые с созданием новых потоков, и делают все упражнение бессмысленным.

Комментарии:

1. Привет, Гистро, я думаю, вы можете добиться того, чтобы количество было закрытым и все равно могло быть уменьшено, как, например, я сделал, конечно, в конце концов, для этого требуется какая-то общая структура между потоками

2. Спасибо за ответ! Недавно я больше работал над этой проблемой. Я пробовал два варианта: 1). использовал clang libomp и 2). используется num_threads(max_thrs) с соответствующим количеством блоков (nthr = 2 или nthr = 16), т. Е. Как в вашем коде и коде @dreamcrash. Интересно, что оба варианта показали худшую производительность по сравнению с моей первоначальной версией (я имею в виду с количеством итераций, равным nthr)

3. Я имею в виду, что #pragma parallel для num_threads(nthr) для (int i=0;i < nthr; i ) с gcc libgomp показывает лучшую производительность, чем #pragma parallel для num_threads(nthr) с clang libomp и чем #pragma parallel для num_threads(MAX_THRS) с gcc libgomp.

4. Кстати, в обоих вариантах (с clang-libomp и с num_threads (MAX_THRS) gcc-ligomp) — я проверил с помощью gdb, что потоки действительно уничтожаются только в самом конце программы. Знаете ли вы, почему это может быть так? @dreamcrash

5. Извините, я имел в виду, что во всех #pragmas в моих комментариях выше цикл предназначен для (int i=0;i < nthr; i ) . Я имею в виду, что я изменил свой цикл, чтобы всегда иметь нужное количество итераций (равное количеству потоков, которые я хочу — nthr).

Ответ №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; , которое необходимо исправить. В исходном коде вы могли бы легко исправить это, используя предложение сокращения, а именно #pragma omp parallel for num_threads(thr) reduction(sum:count) . В коде с руководством для вас это можно решить следующим образом:

 #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 = %dn", count);
    return 0;
} 
 

Я бы сказал, что в большинстве случаев в каждой параллельной области будет использоваться одинаковое количество потоков. Таким образом, такой тип шаблона не должен быть большой проблемой.