Молодогвардейцев 454015 Россия, Челябинская область, город Челябинск 89085842764
MindHalls logo

Основные директивы OpenMP с примерами

Доброго времени суток, друзья! Как вы могли заметить, я иногда балуюсь с параллельным программированием посредством OpenMP. На данный момент готова заметка о том, как установить и настроить omp в Clion. А из реализаций есть параллельное умножение матриц. Я посчитал, что самое время собрать небольшую шпаргалку по наиболее часто используемым директивам и их параметрам, чтобы можно было сюда заглянуть и освежить их в памяти. Постараюсь на каждую директиву добавить по небольшому, чисто символическому синтаксическому примеру.

Не секрет, что OpenMP доступен на языках C/C++ и Fortran(даже слышал, что и на Java есть), но я буду писать примеры только для C/C++, уж не сердитесь, теория для них все равно одинаковая. Предлагаю ни секунды не терять и приступать к делу.

Общий синтаксис вызова директив OpenMP

Любые дополнительные директивы вызываются с помощью стандартной директивы #pragma, и OpenMP не исключение. Следовательно, для того, чтобы обратиться к директиве нужно написать #pragma omp, и назвать искомую директиву. Справедлива следующая конструкция вызова.

#pragma omp директива [опция1, опция2, ...]

Где «директива» — имя директивы, а опции являются необязательным для вызова, их у разных директив может быть разное количество, о них я тоже вкратце расскажу.

Директива parallel

Самая основная директива, служит для создания нитей(threads). Блок, который следует за директивой будет выполнен параллельно. Количество вновь созданных нитей можно установить с помощью вызова функции omp_set_num_threads() передав ей целое число соответствующее желаемому количеству. Узнать номер нити в коде можно вызвав функцию omp_get_thread_num().

Важно! Если написать вложенную директиву parallel, то каждая из созданных нитей, встретив директиву, создаст еще такое же количество дополнительных нитей, будьте внимательны.

#pragma omp parallel [опция1, опция2, ...]
{
    //блок
}

Опции директивы parallel

shared(переменные) — в скобочках можно явно перечислить список переменных, которые будут общими для всех нитей, хотя по умолчанию все переменные, объявленные до блока таковыми являются.

private(переменные) — в скобочках можно указать список переменных, которые будут частными для каждой нити.

if(условие) — проверка условия, если true, то выполняется директива, если false, то код исполняется в прежнем режиме, без создания нитей. Может быть только одна такая опция на директиву.

Пример использования

omp_set_num_threads(4); //Установить количество нитей
#pragma omp parallel shared(num) //В этот момент создается 4 нити, переменная num у них будет общей
 {
 cout << omp_get_thread_num() << endl; //Вывести номер нити
 someFunc(num); //Функцию кинутся выполнять все нити параллельно
 }
omp_set_num_threads(4); //Установить количество нитей
#pragma omp parallel //В этот момент создается 4 нити
{
    //А в этот момент каждая из 4 нитей создаст еще 4,
    // итого 16 нитей буду выполнять следующий блок
    #pragma omp parallel 
    {
        //блок
    }
}

Директива for

Используется для явного распараллеливания следующего цикла for, при этом каждая нить начнет со своего индекса. Если не указывать директиву, то цикл будет пройден каждой нитью полностью от начала и до конца.

#pragma omp for [опция1, опция2]
for(int i = 0; i < 100; i++)

nowait — опция позволяет нитям избежать барьерной синхронизации в конце цикла, которая выполняется по умолчанию.

Пример использования

omp_set_num_threads(4);
#pragma omp for
//в этот момент каждая из нитей возьмет себе по индексу и начнет исполнение цикла
for(int i = 0; i < 8; i++) {
    //блок
}

Нужно очень внимательно относиться к распараллеливанию циклов в программе, точно знать, что алгоритм не пострадает от того, что не все нити полностью пройдут по циклу.

Директива single

Служит для того, чтобы выполнить некий блок в параллельной области только один раз. Часто используется для выводов, чтобы только одна нить печатала информацию.

#pragma omp single

Пример использования

omp_set_num_threads(4);
#pragma omp for
for(int i = 0; i < 8; i++) {
    #pragma omp single
    cout << " i = " << i << endl;
}

При этом какая именно нить выполнит участок кода, заранее мы никак не узнаем.

Директива master

Аналогична директиве single, но мы точно знаем, что блок выполнится мастер-нитью. Мастером является та нить, которая породила все остальные, ее порядковый номер равен 0.

Директива critical

Похожа на две предыдущие. Разница заключается в том, что блок после директивы critical выполнится всеми нитями, но по очереди. Если блок уже исполняется любой нитью, то все остальные блокируются на входе и ждут завершения. После завершения работы, случайным образом из списка заблокированных выбирается новая. Таким образом в один момент блок будет исполняться только одной нитью.

Пример использования

omp_set_num_threads(4);
#pragma omp parallel
{
    int n = 1;
    #pragma omp critical //Тут образуется очередь на вход
    {
        n++;
        cout << "n = " << n << endl;
    }
}

Директива barrier

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

Пример использования

omp_set_num_threads(4);
#pragma omp parallel
{
    int n = 1000;
    for(int i = 0; i < n; i++) {
        someFunc();
    }

    //Исполнение кода заблокируется до тех пор, пока все нити не пройдут цикл
    #pragma omp barrier 

    someFunc2();
}

Заключение

Это далеко не самый полный список, признаю, сейчас я записал сюда только самые основные директивы и опции, которые использую, использовал и буду использовать. Поэтому вполне вероятно, что список, описания и примеры будут пополняться. Но на сегодня у меня все, спасибо за внимание!