Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Экзамен ОС 2018

.pdf
Скачиваний:
117
Добавлен:
29.01.2018
Размер:
4.67 Mб
Скачать

Оглавление

24. Атомарные операции и lockless программирование. Реализация многопоточности с использованием технологии OpenMP, блокировки и синхронизация потоков в OpenMP.

Атомарные операции и lockless программирование.

Lockless программирование

Lockless программирование – разработка неблокирующих многопоточных приложений.

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

Достоинство – повышенная производительность многопоточных приложений на многоядерных процессорах.

Атомарные операции как lockless-инструмент

Простейшим способом lockless-программирования является активное использованием атомарных операций при конкурентном доступе нескольких потоков к общим переменным.

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

Виды атомарных операций

Все инструкции вида Операция Регистр-Регистр можно считать атомарными так как регистры за пределами вычислительного процессорного ядра не видны.

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

Сохранение данных из регистра общего назначения в память по выровненному адресу.

Специальные операции для атомарной работы (например, cmpxchg, tsl).

Многие команды вида Чтение-Модификация-Запись могут быть сделаны искусственно атомарными с помощью операции блокировки шины (префикс lock).

Реализация атомарных операций в Windows 2000+

Для увеличения значения целочисленных переменных –

InterlockedIncrement​, InterlockedIncrement64​.

Для уменьшения значения целочисленных переменных –

InterlockedDecrement,​InterlockedDecrement64​.

Оглавление

Оглавление

Для изменения значений целочисленных переменных –

InterlockedExchange, InterlockedExchange64, InterlockedExchangeAdd, InterlockedExchangePointer​.

Для изменения значений целочисленных переменных со сравнением –

InterlockedCompareExchange, InterlockedCompareExchangePointer​.

Переупорядочивание и модель памяти

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

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

Говоря о переупорядочивании, мы приходим к термину модели памяти (memory consistency model или просто memory model).

«Сильная» и «слабая» модели памяти

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

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

Барьеры памяти и оптимизации

Для борьбы с переупорядочиванием применяются так называемые барьеры памяти (memory barrier или memory fence).

В случае барьера для компилятора применяют еще и термин барьер оптимизации (optimization barrier).

Барьеры могут быть как явными, так и не явными (с точки зрения программиста).

Кроме того барьеры могут быть полными, двухсторонними и односторонними.

Полные барьеры

Полный барьер (full fence) предотвращает любые переупорядочивания операций чтения или записи через него.

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

Данный барьер предлагает использование инструкции mfence для x86/x64 архитектуры.

Двухсторонние барьеры

Двусторонние барьеры (Store fence и Load fence) предотвращают переупорядочивание лишь одного вида операций.

Барьер записи (store fence) не позволяет переупорядочивать через себя операции записи.

Барьер чтения (load fence) не позволяет переупорядочивать через себя операции чтения соответственно.

Оглавление

Оглавление

На x86/x64 архитектурах данные барьеры реализованы в инструкциях lfence и sfence.

Односторонние барьеры

Односторонние барьеры обычно реализуют одну из двух семантик – write release (store release) или read acquire (load acquire).

Write release семантика предотвращает любое переупорядочивание чтения и записи через барьер до барьера (запрет ↓), но не предотвращает переупорядочивания после него.

Read acquire семантика предотвращает переупорядочивание чтения и записи через барьер после барьера (запрет ↑), но не предотвращает переупорядочивание до него.

Управление переупорядочиванием в MS VC

MS VC для предотвращения переупорядочивания инструкций со стороны компилятора предлагает использовать _ReadBarrier(), _WriteBarrier(), _ReadWriteBarrier().

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

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

Неявные барьеры в MS VC 2005

В случае MS VC 2005+ ключевое слово volatile приобретает значение, выходящее за рамки С++ стандарта.

Запись в volatile переменную всегда реализует семантику одностороннего барьера write release.

Чтение из volatile переменной всегда реализует семантику одностороннего барьера read acquire.

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

Производительность lockless приложений

MemoryBarrier () занимает 20-90 циклов.

InterlockedIncrement () занимает 36-90 циклов.

Вхождение или освобождение критической секции может занимать 40-100 циклов.

Захват или освобождение мьютекса может занимать 750-2500 циклов.

Оглавление

Оглавление

Реализация многопоточности с использованием технологии OpenMP Компоненты OpenMP

Директивы pragma

Функции исполняющей среды OpenMP

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

Директивы pragma

Директивы pragma, как правило, указывают компилятору реализовать параллельное выполнение блоков кода. Все эти директивы начинаются с

#pragma omp.

Как и любые другие директивы pragma, они игнорируются компилятором, не поддерживающим технологию OpenMP.

Функции run-time OpenMP

Функции библиотеки run-time OpenMP позволяют:

контролировать и просматривать параметры параллельного приложения (например, функция omp_get_thread_num​возвращает номер потока, из которого вызвана);

использовать синхронизацию (например, omp_set_lock​устанавливает блокировку доступа к критической секции).

Чтобы задействовать эти функции библиотеки OpenMP периода выполнения

(исполняющей среды), в программу нужно включить заголовочный файл omp.h. Если вы используете в приложении только OpenMP-директивы pragma, включать этот файл не требуется.

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

Переменные окружения контролируют поведение приложения.

Например, переменная OMP_NUM_THREADS задает количество потоков в параллельном регионе.

Формат директивы pragma

Для реализации параллельного выполнения блоков приложения нужно просто добавить в код директивы pragma и, если нужно, воспользоваться функциями библиотеки OpenMP периода выполнения.

Директивы pragma имеют следующий формат:

#pragma omp <директива> [раздел [ [,] раздел]...]

OpenMP поддерживает директивы parallel, for, parallel for, section, sections, single, master, critical, flush, ordered и atomic, которые определяют или механизмы разделения работы или конструкции синхронизации.

Далее мы рассмотрим простейший пример с использованием директив parallel, for, parallel for.

Реализация параллельной обработки

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

Оглавление

Оглавление

#pragma omp parallel [раздел[ [,] раздел]...] структурированный

блок

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

Создается набор (team) из N потоков; исходный поток программы является основным потоком этого набора (master thread) и имеет номер 0.

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

В качестве примера рассмотрим классическую программу «Hello World»:

#pragma omp parallel

{

printf("Hello World\n");

}

Директива #pragma omp for

Директива #pragma omp for сообщает, что при выполнении цикла for в параллельном регионе итерации цикла должны быть распределены между потоками группы.

Следует отметить, что в конце параллельного региона выполняется барьерная синхронизация (barrier synchronization). Иначе говоря,

достигнув конца региона, все потоки блокируются до тех пор, пока последний поток не завершит свою работу.

Директива #pragma omp parallel for

#pragma omp parallel + #pragma omp for

=

#pragma omp parallel for

Пример:

Оглавление

Оглавление

Распараллеливание при помощи директивы sections

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

Директивы section определяют секции, которые могут быть выполнены параллельно.

#pragma omp sections [<параметр> ...]

{

#pragma omp section <блок_программы> #pragma omp section <блок_программы>

}

Директива single

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

Данную возможность в OpenMP обеспечивает директива single.

#pragma omp single [<параметр> ...] <блок_программы>

Оглавление

Оглавление

Задание числа потоков

Чтобы узнать или задать число потоков в группе, используйте функции omp_get_num_threads и omp_set_num_threads.

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

Метод omp_set_num_thread ​задает число потоков для выполнения следующего параллельного региона, который встретится текущему выполняемому потоку (статическое планирование).

Область видимости переменных

Общие переменные (shared) –

доступны всем потокам.

Частные переменные (private) –

создаются для каждого потока только на время его выполнения.

Правила видимости переменных:

все переменные, определенные вне параллельной области – общие;

все переменные, определенные внутри параллельной области – частные.

Директивы указания области видимости переменных

Для явного указания области видимости используются следующие параметры директив:

shared(имя_переменной, …)

общие переменные

private(имя_переменной, …)

частные переменные

Примеры:

#pragma omp parallel shared(buf)

#pragma omp for private(i, j)

Алгоритмы планирования

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

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

Кроме того OpenMP поддерживает и другие механизмы планирования:

динамическое планирование (dynamic scheduling);

планирование в период выполнения (runtime scheduling);

управляемое планирование (guided scheduling);

автоматическое планирование (OpenMP 3.0) (auto).

Чтобы задать один из этих механизмов планирования, используйте раздел schedule​в директиве #pragma omp for или #pragma omp parallel for.

Оглавление

Оглавление

Формат этого раздела выглядит так:

schedule(алгоритм планирования[, число итераций])

Динамическое планирование

При динамическом планировании каждый поток выполняет указанное число итераций (по умолчанию равно 1).

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

Последний набор итераций может быть меньше, чем изначально заданный.

Управляемое планирование

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

число_выполняемых_потоком_итераций = max (

Число_нераспределенных_итераций / omp_get_num_threads(), число итераций

);

Примеры:

#pragma omp parallel for schedule(dynamic, 15) for(int i = 0; i < 100; ++i) ...

#pragma omp for schedule(guided, 10)

for(int i = 0; i < 100; ++i) ...

Сравнение динамического и управляемого планирования

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

При статическом планировании нет никакого способа, позволяющего сбалансировать нагрузку на разные потоки.

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

Планирование в период выполнения

Планирование в период выполнения – это способ динамического выбора в ходе выполнения одного из трех описанных ранее алгоритмов.

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

Если в разделе schedule указан параметр runtime, исполняющая среда OpenMP использует алгоритм планирования, заданный для конкретного цикла for при помощи переменной OMP_SCHEDULE.

Оглавление

Оглавление

Переменная OMP_SCHEDULE имеет формат «тип[,число итераций]», например:

set OMP_SCHEDULE=dynamic,8

Автоматическое планирование

Способ распределения итераций цикла между потоками определяется реализацией компилятора.

На этапе компиляции программы или во время ее выполнения определяется оптимальный способ распределения.

#pragma omp parallel for schedule (auto) for(int i = 0; i < 100; i++)

Блокировки и синхронизация потоков в OpenMP.

Блокировки (замки)

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

OpenMP два типа блокировок:

простые блокировки;

рекурсивные (nestable) блокировки.

Блокировки обоих типов могут находиться в одном из трех состояний:

неинициализированном;

заблокированном;

Разблокированном.

Простые блокировки (omp_lock_t) не могут быть установлены более одного раза, даже тем же потоком.

Рекурсивные блокировки (omp_nest_lock_t) идентичны простым с тем исключением, что, когда поток пытается установить уже принадлежащую ему рекурсивную блокировку, он не блокируется. Кроме того, OpenMP ведет учет ссылок на рекурсивные блокировки и следит за тем, сколько раз они были установлены.

OpenMP предоставляет подпрограммы, выполняющие операции над этими блокировками. Каждая такая функция имеет два варианта: для простых и для рекурсивных блокировок.

Оглавление

Оглавление

Функции для работы с блокировками в OpenMP и Win32

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

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

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

Неявная барьерная синхронизация

Неявная барьерная синхронизация выполняется также в конце каждого блока

#pragma omp for, #pragma omp single и #pragma omp sections.

Чтобы отключить неявную барьерную синхронизацию в каком-либо из этих трех блоков разделения работы, укажите раздел nowait.

Синхронизация потоков в конце цикла for не будет выполняться, хотя в конце параллельного региона они все же будут синхронизированы.

#pragma omp parallel

{

#pragma omp for nowait for(int i = 1; i < size; ++i)

x[i] = (y[i-1] + y[i+1])/2;

}

Явная барьерная синхронизация

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

#pragma omp barrier

Оглавление