Введение в программирование на кластерах

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

Показывать лекцию целиком

Презентацию к данной лекции Вы можете скачать здесь.

Многопоточная модель параллельного программирования

Поток (нить) представляет собой последовательный поток управления (последовательность команд) в рамках одной программы.

При создании процесса порождается главный поток, выполняющий инициализацию процесса. Он же начинает выполнение команд.

Поток и процесс соотносятся следующим образом:

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

    Конкуренция за ресурсы (видимый параллелизм)

    Реальный параллелизм

    Для реализации реального параллелизма требуется соответствующая архитектура – многоядерная или многопроцессорная с общей памятью.

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

  • гонки за данными;
  • блокировки;
  • несбалансированность загрузки.
  • Гонки за данными (data races)

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

  • конфликт "чтение-запись";
  • конфликт "запись-запись".
  • Два способа борьбы с гонками за данными:

  • использование преимущественно локальных по отношению к потоку, а не разделяемых переменных;
  • управление доступом к разделяемым переменным с помощью различных средств синхронизации (они могут быть реализованы с помощью семафоров, событий, критических секций, взаимных блокировок - мьютексов).
  • Блокировки

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

    Условия возникновения тупика

  • доступ к ресурсу эксклюзивен (возможен только одним потоком);
  • поток может удерживать ресурс, запрашивая другой;
  • ни один из конкурирующих потоков не может освободить запрашиваемый ресурс.
  • Масштабируемость

    Число программных потоков должно совпадать с числом аппаратных потоков

    Реализации

    POSIX Threads Windows API низкоуровневые инструменты
    OpenMP высокоуровневые инструменты

    OpenMP. Модель программы и структура

  • Программа состоит из последовательных и параллельных секций.
  • В начальный момент времени создается главная (мастер) нить, выполняющая последовательные секции программы.
  • При входе в параллельную секцию выполняется операция fork, порождающая семейство нитей. Каждая нить имеет свой уникальный числовой идентификатор (главной нити соответствует 0). Все параллельные нити исполняют один код.
  • При выходе из параллельной секции выполняется операция join. Завершается выполнение всех нитей, кроме главной.
  • Директивы компилятора - используются для создания потоков, распределения работы между потоками и их синхронизации. Директивы включаются в исходный текст программы.
  • Подпрограммы библиотеки времени выполнения - используются для установки и определения атрибутов потоков. Вызовы этих подпрограмм включаются в исходный текст программы.
  • Переменные окружения- используются для управления поведением параллельной программы.
  • Привязки к языкам

    Привязка к языку C

    Прагмы, имена функций и переменных окружения OpenMP начинаются с omp, omp_ или OMP_

    Формат директивы: #pragma omp директива [оператор_1[, оператор_2, …]]

    Заголовочный файл omp.h.

    Привязка к языку Fortran

    Директивы компилятора, имена подпрограмм и переменных окружения начинаются с OMP или OMP_.

    Формат директивы компилятора:

    {!|C|*}$OMP директива [оператор_1[, оператор_2, …]]

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

    Директивы

  • parallel
    …
    end parallel

    Границы параллельной секции программы.С данной директивой могут использоваться следующие операции:

    private;
    shared;
    default;
    firstprivate;
    reduction;
    if;
    copyin;
    num_threads
  • #pragma omp for
    цикл for
    

    Границы цикла, исполняемого в параллельном режиме.С данной директивой могут использоваться следующие операции:

    private;
    firstprivate;
    lastprivate;
    reduction;
    schedule;
    ordered;
    nowait.
    
  • sections
    …
    end sections
    

    Вложенные секции программы, задаваемые директивами section, распределяются между нитями. С данной директивой могут использоваться следующие операции:

    private;
    firstprivate;
    lastprivate;
    reduction;
    nowait.
  • section
    

    Определяет часть sections, которая выполняется одной нитью.

  • parallel do
    цикл do
    end parallel do
    

    Объединяет директивы parallel и do.

  • parallel sections
    …
    end parallel sections
    

    Объединяет директивы parallel и sections.

  • critical[(блокировка)]
    …
    end critical[(блокировка)]
    

    Обрамляет блок программы, доступ к которому в любой момент времени может получить только одна нить (критическая секция). Блокировка – необязательное имя критической секции.

  • barrier

    Барьерная синхронизация нитей.

  • atomic

    Объявляет операцию атомарной (то есть, одновременный доступ по записи разных нитей запрещен). Применяется только к оператору, непосредственно следующему после данной директивы. Он может иметь следующий вид:

    x = x {+|-|*|/|.AND.|.OR.|.EQV.|.NEQV.}
      скалярное_выражение_не_содержащее_x
    x = скалярное_выражение_не_содержащее_x 
      {+|-|*|/|.AND.|.OR.|.EQV.|.NEQV.} 
    x = {MAX|MIN|IAND|IOR|IEOR} 
      (x, скалярное_выражение_не_содержащее_x)
    x = {MAX|MIN|IAND|IOR|IEOR} 
      (скалярное_выражение_не_содержащее_x, x)
  • flush[(список переменных)]

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

  • ordered
    …
    end ordered
    

    Сохранение того порядка выполнения итераций цикла, который соответствует последовательному выполнению программы.

  • Операторы

  • private(список переменных)

    Объявляет переменные из списка локальными.

  • firstprivate(список переменных)

    Объявляет переменные из списка локальными и инициализирует их значениями из блока программы, предшествующего данной директиве.

  • lastprivate(список переменных)

    Объявляет переменные из списка локальными и назначает им значения из того блока программы, который был выполнен последним.

  • nowait

    Отменяет барьерную синхронизацию при завершении выполнения параллельной секции.

  • shared(список переменных)

    Объявляет переменные из списка глобальными.

  • reduction(операция|встроенная функция: список переменных)

    Приведение значений локальных переменных из списка с помощью указанной операции или встроенной функции языка.

  • schedule(характер_распределения_итераций[, количество_итераций_цикла])

    Характер распределения итераций цикла между нитями:

  • static – количество итераций цикла, передаваемых для выполнения каждой нити фиксировано и распределяется между нитями по принципу кругового планирования. Если количество итераций не указано, оно полагается равным 1;
  • dynamic – количество итераций цикла, передаваемых для выполнения каждой нити фиксировано. Очередная "порция" итераций передается освободившейся нити;
  • guided – количество итераций цикла, передаваемых для выполнения каждой нити уменьшается. Очередная "порция" итераций передается освободившейся нити;
  • runtime – тип распределения работы определяется во время выполнения программы, например, с помощью переменной окружения OMP_SCHEDULE.
  • Подпрограммы

  • void omp_set_num_threads(int threads);
    
    subroutine omp_set_num_threads(threads)
    integer threads

    Задает количество потоков (threads) при выполнении параллельных секций программы

  • int omp_get_num_threads(void);
    
    integer function omp_get_num_threads()
    

    Возвращает количество потоков, используемых для выполнения параллельной секции.

  • void omp_set_nested(int nested);
    
    subroutine omp_set_nested(nested)
    integer nested
    

    Разрешает или запрещает вложенный параллелизм. По умолчанию запрещен.

  • int omp_get_nested(void);
    
    logical function omp_get_nested()

    Определяет, разрешен ли вложенный параллелизм.

  • void omp_init_lock(omp_lock_t *lock);
    
    subroutine omp_init_lock(lock)
    integer(kind = omp_lock_kind) :: lock

    Инициализирует блокировку, связанную с идентификатором lock, для использования в последующих вызовах.

  • void omp_set_lock(omp_lock_t *lock);
    
    subroutine omp_set_lock(lock)
    integer(kind = omp_lock_kind) :: lock

    Переводит потоки из состояния выполнения в состояние ожидания до тех пор, пока блокировка, связанная с идентификатором lock, не окажется доступной. Поток становится владельцем доступной блокировки.

  • void omp_unset_lock(omp_lock_t *lock);
    
    subroutine omp_unset_lock(lock)
    integer(kind = omp_lock_kind) :: lock

    После выполнения вызова поток перестает быть владельцем блокировки, связанной с идентификатором lock. Если поток не был владельцем блокировки, результат вызова не определен.

  • Таймеры

  • double omp_get_wtime(void);
    
    double precision function omp_get_wtime()

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

  • double omp_get_wtick(void);
    
    double precision function omp_get_wtick()

    Время в секундах, прошедшее между последовательными "тиками". Это время является мерой точности таймера.

  • Переменные окружения

    Переменные окружения задаются следующим образом:

    export ПЕРЕМЕННАЯ=значение (UNIX)
    
    set ПЕРЕМЕННАЯ=значение (Microsoft Windows)
    
  •  OMP_NUM_THREADS

    Задает количество нитей при выполнении параллельных секций программы.

  • OMP_SCHEDULE

    Задает способ распределения итераций циклов между нитями. Возможные значения:

    static;
    dynamic;
    guided.
    

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

    export OMP_SCHEDULE="static, 10"
  • Примеры

  • program omp_example
    integer i, k, N
    real*4 sum, h, x
    print *, "Please, type in N:"
    read *, N
    h = 1.0 / N
    sum = 0.0
    !$OMP PARALLEL DO SCHEDULE(STATIC) REDUCTION(+:sum)
    do i = 1, N
    x = i * h
    sum = sum + 1.e0 * h / (1.e0 + x**2)
    enddo
    print *, 4.0 * sum
    end
  • #include "omp.h"
    #include <stdio.h >
    double f(double x) {
    return 4.0 / (1 + x * x); }
    main () {
    const long N = 100000;
    long i;
    double h, sum, x;
    sum = 0;
    h = 1.0 / N;
    #pragma omp parallel shared(h) {
    #pragma omp for private(x) reduction(+:sum)
    for (i = 0; i < N; i++) {
    x = h * (i + 0.5);
    sum = sum + f(x); } }
    printf("PI = %f\n", sum / N); }
  • Поддержка

    OpenMP поддерживается компиляторами Intel®, GCC и другими.

    Компиляция OpenMP-программ выполняется с ключом -openmp или –fopenmp.

    Вернуться к учебному плану