Что такое «неявная синхронизация» в OpenMP - PullRequest
2 голосов
/ 05 апреля 2019

Что такое «неявная синхронизация» в OpenMP и как ее обнаружить?Мой учитель сказал, что

#pragma omp parallel
printf(“Hello 1\n”);

Имеет неявную синхронизацию.Зачем?А как ты это видишь?

1 Ответ

5 голосов
/ 06 апреля 2019

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

В openmp существует два вида синхронизации: явная и неявная.Явная синхронизация выполняется с помощью специальной конструкции openmp, которая позволяет создать барьер : #pragma omp barrier.Барьер - это параллельная конструкция, которая может проходить только по всем потокам одновременно.Таким образом, после барьера вы точно знаете состояние всех потоков и, что более важно, какой объем работы они проделали.

Неявная синхронизация выполняется в двух ситуациях:

  • в конце параллельной области .Openmp использует модель fork-join .Когда программа запускается, создается один поток ( основной поток ).Когда вы создаете параллельный раздел с помощью #pragma omp parallel, создается несколько потоков ( fork ).Эти потоки будут работать одновременно и в конце параллельной секции будут уничтожены ( join ).Таким образом, в конце параллельной секции у вас есть синхронизация, и вы точно знаете состояние всех потоков (они завершили свою работу).Это то, что происходит в приведенном вами примере.Параллельный раздел содержит только printf(), и в конце программа ожидает завершения всех потоков, прежде чем продолжить.

  • в конце некоторых конструкций openmp как #pragma omp for или #pragma omp sections, существует неявный барьер.Никакая нить не может продолжать работать, пока все нити не достигли барьера.Важно точно знать, какая работа была проделана различными потоками.

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

#pragma omp parallel
{
  #pragma omp for
  for(int i=0; i<N; i++)
    A[i]=f(i); // compute values for A
  #pragma omp for
  for(int j=0; j<N/2; j++)
    B[j]=A[j]+A[j+N/2];// use the previously computed vector A
} // end of parallel section

Поскольку все потоки работаютасинхронно, вы не знаете, какие потоки закончили создавать свою часть вектора A.Без синхронизации существует риск того, что поток быстро завершит свою часть первого цикла for, войдет во второй цикл for и получит доступ к элементам вектора A, пока потоки, которые должны их вычислять, все еще находятся впервый цикл и не вычислили соответствующее значение A[i].

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

Но в некоторых ситуациях синхронизация не требуется.Например, рассмотрим следующий код:

#pragma omp parallel
{
  #pragma omp for
  for(int i=0; i<N; i++)
    A[i]=f(i); // compute values for A
  #pragma omp for
  for(int j=0; j<N/2; j++)
    B[j]=g(j);// compute values for B
} // end of parallel section

Очевидно, что два цикла полностью независимы, и не имеет значения, правильно ли вычислен A для запуска второго цикла for.Таким образом, синхронизация ничего не дает для правильности программы, и добавление барьера синхронизации имеет два основных недостатка:

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

  2. Синхронизация стоит дорого.Простой способ реализовать барьер - увеличить глобальный счетчик при достижении барьера и дождаться, пока значение счетчика не станет равным количеству потоков omp_get_num_threads().Чтобы избежать гонок между потоками, увеличение глобального счетчика должно выполняться с атомарным чтением-изменением-записью, которое требует большого количества циклов, а ожидание правильного значения счетчика обычно выполняется с помощью спин-блокировки, которая приводит к потере процессорациклы.

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

#pragma omp parallel
{
  #pragma omp for nowait  // nowait suppresses implicit synchronisations. 
  for(int i=0; i<N; i++)
    A[i]=f(i); // compute values for A
  #pragma omp for
  for(int j=0; j<N/2; j++)
    B[j]=g(j);// compute values for B
} // end of parallel section

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

...