Разница между omp critical и omp single

Я пытаюсь понять точную разницу между #pragma omp critical и #pragma omp single в OpenMP:

Определения Microsoft для них:

  • Одиночный: Позволяет указать, что раздел кода должен быть выполнен в один поток, не обязательно основной поток.
  • Критический: указывает, что код выполняется только в одном потоке время.

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

Как насчет разницы? Похоже, что критические заботятся о времени исполнения, но не одиноки! Но я не вижу никакой разницы на практике! Означает ли это, что вид ожидания или синхронизации для других потоков (которые не входят в этот раздел) рассматривается в критическом состоянии, но нет ничего, что удерживало бы другие потоки в одном? Как это может изменить результат на практике?

Я ценю, если кто-нибудь может прояснить это мне, особенно на примере. Спасибо!

Ответ 1

single и critical - это две очень разные вещи. Как вы упомянули:

  • single указывает, что раздел кода должен выполняться одним потоком (не обязательно основной поток)
  • critical указывает, что код выполняется одним потоком за раз

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

Например, следующий код

int a=0, b=0;
#pragma omp parallel num_threads(4)
{
    #pragma omp single
    a++;
    #pragma omp critical
    b++;
}
printf("single: %d -- critical: %d\n", a, b);

напечатает

single: 1 -- critical: 4

Надеюсь, теперь вы видите разницу.

Для полноты я могу добавить, что:

  • master очень похож на single с двумя отличиями:
    • master будет выполняться только мастером, тогда как single может выполняться тем, какая нить сначала достигает области; и
    • single имеет неявный барьер после завершения области, где все потоки ожидают синхронизации, а master не имеет.
  • atomic очень похож на critical, но ограничен для выбора простых операций.

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

Ответ 2

single и critical относятся к двум совершенно различным классам конструкций OpenMP. single представляет собой конструкцию совместного использования, наряду с for и sections. Конструкции Worksharing используются для распределения определенной работы между потоками. Такие конструкции являются "коллективными" в том смысле, что в правильных программах OpenMP все потоки должны встречаться с ними при выполнении и, кроме того, в одном и том же порядке, включая конструкторы barrier. Три конструкции для коллекционирования охватывают три разных общих случая:

  • for (конструкция цикла a.k.a.) автоматически распределяет итерации цикла между потоками - в большинстве случаев все потоки получают работу;
  • sections распределяет последовательность независимых блоков кода среди потоков - некоторые потоки выполняют работу. Это обобщение конструкции for, поскольку цикл с 100 итерациями может быть выражен, например, 10 секций петель с 10 итерациями каждый.
  • single выделяет блок кода для выполнения только одним потоком, часто первым, с которым он сталкивается (деталь реализации) - только один поток получает работу. single в значительной степени эквивалентен sections только с одним разделом.

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

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

  • critical предотвращает условия гонки, предотвращая одновременное выполнение кода среди потоков в так называемой конкурирующей группе. Это означает, что все потоки из всех параллельных областей, сталкиваясь с аналогичными критическими конструкциями, становятся сериализованы;
  • atomic превращает некоторые простые операции памяти в атомные, обычно используя специальные инструкции по сборке. Atomics завершается сразу как единый нерушимый блок. Например, атом, считываемый из некоторого местоположения одним потоком, который одновременно происходит с атомарной записью в одно и то же место другим потоком, либо вернет старое значение, либо обновленное значение, но никогда не будет промежуточным месивом битов как из старого, так и из новых значений;
  • master выделяет блок кода для выполнения главным потоком (поток с идентификатором 0). В отличие от single, в конце конструкции нет неявного барьера, и также нет требования, чтобы все потоки столкнулись с конструкцией master. Кроме того, отсутствие скрытого барьера означает, что master не очищает представление разделяемой памяти потоков (это важная, но очень слабо понятная часть OpenMP). master является в основном сокращением для if (omp_get_thread_num() == 0) { ... }.

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

Далее следует иллюстрация вышеприведенных понятий. Следующий код:

#pragma omp parallel num_threads(3)
{
   foo();
   bar();
   ...
}

приводит к чему-то вроде:

thread 0: -----< foo() >< bar() >-------------->
thread 1: ---< foo() >< bar() >---------------->
thread 2: -------------< foo() >< bar() >------>

(поток 2 преднамеренно опоздал)

Имея вызов foo(); внутри конструкции single:

#pragma omp parallel num_threads(3)
{
   #pragma omp single
   foo();
   bar();
   ...
}

приводит к чему-то вроде:

thread 0: ------[-------|]< bar() >----->
thread 1: ---[< foo() >-|]< bar() >----->
thread 2: -------------[|]< bar() >----->

Здесь [ ... ] обозначает объем конструкции single и | - это неявный барьер на своем конце. Обратите внимание, как поток latecomer 2 заставляет все остальные потоки ждать. В потоке 1 выполняется вызов foo() в качестве примера. Рабочая среда OpenMP решает назначить задание первому потоку, чтобы встретить конструкцию.

Добавление предложения nowait может удалить неявный барьер, что приведет к чему-то вроде:

thread 0: ------[]< bar() >----------->
thread 1: ---[< foo() >]< bar() >----->
thread 2: -------------[]< bar() >---->

Имея вызов foo(); в анонимной конструкции critical:

#pragma omp parallel num_threads(3)
{
   #pragma omp critical
   foo();
   bar();
   ...
}

приводит к чему-то вроде:

thread 0: ------xxxxxxxx[< foo() >]< bar() >-------------->
thread 1: ---[< foo() >]< bar() >------------------------->
thread 2: -------------xxxxxxxxxxxx[< foo() >]< bar() >--->

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

Критические конструкции разных имен не синхронизируются друг с другом. Например:.

#pragma omp parallel num_threads(3)
{
   if (omp_get_thread_num() > 1) {
     #pragma omp critical(foo2)
     foo();
   }
   else {
     #pragma omp critical(foo01)
     foo();
   }
   bar();
   ...
}

приводит к чему-то вроде:

thread 0: ------xxxxxxxx[< foo() >]< bar() >---->
thread 1: ---[< foo() >]< bar() >--------------->
thread 2: -------------[< foo() >]< bar() >----->

Теперь поток 2 не синхронизируется с другими потоками, потому что его критическая конструкция называется по-разному и поэтому делает потенциально опасный одновременный вызов в foo().

С другой стороны, анонимные критические конструкции (и в общих конструкциях с тем же именем) синхронизируются друг с другом независимо от того, где они находятся в коде:

#pragma omp parallel num_threads(3)
{
   #pragma omp critical
   foo();
   ...
   #pragma omp critical
   bar();
   ...
}

и итоговую временную шкалу выполнения:

thread 0: ------xxxxxxxx[< foo() >]< ... >xxxxxxxxxxxxxxx[< bar() >]------------>
thread 1: ---[< foo() >]< ... >xxxxxxxxxxxxxxx[< bar() >]----------------------->
thread 2: -------------xxxxxxxxxxxx[< foo() >]< ... >xxxxxxxxxxxxxxx[< bar() >]->