OpenMP: общие переменные в единой конструкции nowait

Итак, у меня есть этот небольшой фрагмент кода:

int a = 10;
bool finished = false;

#pragma omp parallel num_threads(3) shared(a, finished)
{
    while(!finished) {

        #pragma omp single nowait
        {
            printf("[%d] a is: %d\n", omp_get_thread_num(), a);
            a--;
            finished = true;
        }

    }
}

На выходе

[0] a is: 10
[2] a is: 10
[1] a is: 10

Чего я совсем не ожидал. Я понимаю, что все потоки могут превратиться в единую конструкцию до выхода из цикла while, но почему все они говорят одно и то же? Второй поток, который входит, должен иметь a = 9, а третий - a = 8. Я пробовал #pragma omp flush и #pragma omp atomic на a, но без толку. Я хотел бы использовать a для сравнения в этом блоке (т.е. if (a == 10)), поэтому очень важно, чтобы значение обновлялось, когда другой поток входит в единственный блок. Что я делаю неправильно?


person Circus    schedule 23.02.2016    source источник


Ответы (3)


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

Это означает, что первый поток, получивший конструкцию single, выполнит блок в первой итерации. Остальные потоки пропустят блок и немедленно вернутся к конструкции single на своей второй итерации. Затем один из двух оставшихся потоков также входит в блок (поскольку это новая итерация и, следовательно, еще одно появление блока). Другой снова пропускает и сразу же входит в свою третью итерацию. В конце все потоки выполняются почти одновременно и печатают начальное значение a, поскольку ни одному из потоков не удалось уменьшить a до этого момента времени.

Вы увидите a изменение, если поменяете местами операторы внутри блока single следующим образом. Однако значения и их порядок не будут определены.

#pragma omp single nowait
{
    a--;
    printf("[%d] a is: %d\n", omp_get_thread_num(), a);
    finished = true;
}
person user0815    schedule 11.04.2016

Я не верю, что вам нужен один поток, выполняющий ваш блок кода, скорее вы хотите убедиться, что каждый поток не топчет данные других потоков в a и finished. Это достигается с помощью #pragma omp critical. Кроме того, у вас не может быть предложения nowait, потому что по определению другие потоки не войдут в блок кода, пока другой поток не завершит критическую область.

while(!finished) {

    // only 1 thread will enter this region at a time
    #pragma omp critical
    {
        printf("[%d] a is: %d\n", omp_get_thread_num(), a);
        a--;
        finished = true;
    }

}

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

person NoseKnowsAll    schedule 23.02.2016
comment
Это не весь мой алгоритм, это просто упрощенный пример того, к чему сводится проблема. Я использую единственную конструкцию nowait для отправки данных, которую может делать только один поток за раз. Пока один поток находится в единственной конструкции nowait, другие потоки не выполняют другую работу. Критический не будет работать для меня, поскольку другие потоки будут ждать входа в конструкцию вместо выполнения работы. - person Circus; 24.02.2016
comment
Если вы используете это для распределения работы, вам следует вместо этого взглянуть на задачи OpenMP, поскольку они решают эту проблему без необходимости писать весь этот ненадежный механизм. - person Jim Cownie; 24.02.2016
comment
Хотелось бы, только у задач столько зависимостей и я не могу использовать OpenMP 4.0 - person Circus; 24.02.2016
comment
@Circus, явные задачи (без зависимостей) являются частью OpenMP 3.0. Единственная причина не использовать задачи - это если вы хотите оставаться совместимым с MS Visual Studio, которая поддерживает только OpenMP 2.0. - person Hristo Iliev; 24.02.2016

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

В любом случае, я попытался немного экстраполировать и расширил ваш пример, добавив оператор printf() за пределами блока, который также печатает значение a, чтобы увидеть, как это передается в другие потоки. Более того, поскольку вы использовали директиву single, я предположил, что вам нужен только один поток, выполняющий блок, даже если он находится в цикле while(). Так что это выглядело очень хорошо подходящим для использования блокировок OpenMP ...

Вот что я придумал:

#include <stdio.h>
#include <omp.h>
#include <unistd.h>

int main() {
    int a = 10;
    bool finished = false;
    omp_lock_t lock;
    omp_init_lock( &lock );
    #pragma omp parallel num_threads( 3 ) shared( a, finished )
    {
        while( !finished ) {
            if ( omp_test_lock( &lock ) ) {
                printf( "[%d] a is: %d\n", omp_get_thread_num(), a );
                #pragma omp atomic update
                a--;
                usleep( 10 );
                finished = true;
                #pragma omp flush( finished )
                omp_unset_lock( &lock );
            }
            #pragma omp flush( finished, a )
            printf( "[%d] outside of if block, a is: %d\n", omp_get_thread_num(), a );
        }
    }
    return 0;
}

Я добавил вызов usleep(), чтобы немного задержать выполнение инструкций внутри блока if и дать другим потокам возможность что-то напечатать. Я тестировал его с помощью gcc версий 4.9 и 5.3 и компилятора Intel 16.0, и все 3 дают мне одинаковый результат (очевидно, с некоторыми вариациями в порядке и количестве распечаток между запусками).

Результат выглядит так:

~/tmp$ icpc -fopenmp omplock.cc
~/tmp$ ./a.out 
[0] a is: 10
[1] outside of if block, a is: 10
[1] outside of if block, a is: 9
[1] outside of if block, a is: 9
[1] outside of if block, a is: 9
[1] outside of if block, a is: 9
[1] outside of if block, a is: 9
[2] outside of if block, a is: 10
[1] outside of if block, a is: 9
[0] outside of if block, a is: 9

Отвечает ли этот подход вашим потребностям?

person Gilles    schedule 24.02.2016
comment
Спасибо! Я пробовал похожие вещи и получил похожие результаты. Меня больше интересует, почему он ведет себя так, как в моем примере. Я полагал, что только один поток за раз сможет войти в единую конструкцию, и как только другой поток войдет в нее, a уже должен быть обновлен? Я не понимаю, зачем мне нужна блокировка внутри сингла, чтобы он работал, так как в любом случае одновременно может быть только один поток - person Circus; 24.02.2016