>
>
OpenMP и исключения (exceptions)

Андрей Карпов
Статей: 671

OpenMP и исключения (exceptions)

Мы продолжаем развивать наш статический анализатор VivaMP, и на этот раз хочется рассказать о диагностике ошибок, связанных с использованием исключений (exception) языка Си++ в параллельных регионах (parallel regions).

Поддержка VivaMP была прекращена в 2014 году. По всем возникшим вопросам вы можете обратиться в нашу поддержку.

Под параллельным регионом имеются виду фрагмент программы, который делится на параллельно выполняемые нити. Параллельно выполняемые нити формируют такими директивами OpenMP как for и sections.Использовать исключения внутри параллельных регионов можно. Но исключения не должны покидать эти параллельные регионы. Исключения должны быть пойманы и обработаны внутри параллельного региона с использованием конструкций try/catch. Если исключение выйдет за приделы параллельного региона, то это приведет к сбою и скорее всего к аварийному завершению программы. Рассмотрим пример некорректного кода:

#pragma omp parallel for num_threads(4)
for(int i = 0; i < 4; i++)
{
  //...
  throw 1;
}

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

size_t errCount = 0;
#pragma omp parallel for num_threads(4) reduction(+: errCount)
for(int i = 0; i < 4; i++)
{
  try {
    //...
    throw 1;
  }
  catch (...)
  {
    ++errCount;
  }
}
if (errCount != 0)
  throw 1;

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

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

#pragma omp parallel for num_threads(4)
for(int i = 0; i < 4; i++)
{
  float *ptr = new float[10000];
  delete [] ptr;
}

Такой код может годами надежно работать, а может привести к аварийному завершению программы, если в какой-то момент оператор 'new' не сможет выделить объем необходимой памяти. Согласно стандарту языка Си++ оператор new выбрасывает исключение std::bad_alloc если не может выделить необходимый объем памяти. Такой подход позволяет не проверять, выделена ли необходимая память, как это делается в случае использования функции malloc, а сразу начать работу с ней. Если же память не выделена, то программа обработает эту ситуацию в нужном месте. В случае с параллельным регионом необходима дополнительная работа, чтобы корректно обработать ошибку выделения памяти внутри самого региона. Исправленный пример:

#pragma omp parallel for num_threads(4) reduction(+: errCount)
for(int i = 0; i < 4; i++)
{
  try {
    float *ptr = new float[10000];
    delete [] ptr;
  }
  catch (std::bad_alloc &)
  {
    //process error
  }
}

Я думаю, что вы уже догадались, что использование функций внутри параллельных секций дело тоже опасное и неблагодарное. Необходимо или быть уверенным, что функции не генерируют исключений, или обертывать их в try/catch. Неприятный момент в том, что если на момент написания параллельного кода, используемые в нем функции не генерировали исключение, то со временем это может измениться и нужно быть очень аккуратным.

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

V1301. The 'throw' keyword cannot be used outside of a try..catch block in a parallel section

V1302. The 'new' operator cannot be used outside of a try..catch block in a parallel section.

V1303. The FOO function which throws an exception cannot be used in a parallel section outside of a try..catch block.

Диагностическое сообщение V1301 укажет на ошибку в первом примере, а V1302 диагностирует ошибки вызова оператора 'new' вне обработчика исключений. С V1303 все немного сложнее. Сейчас анализатор VivaMP будет предупреждать только о вызове функций, явно помеченных как бросающих исключения, то есть:

void MyThrowFoo() throw(...) { }

Функции не отмеченные "throw(...)" тоже могут бросать исключения, но они не диагностируются как опасные. Этот шаг сделан сознательно, чтобы уменьшить количество лишних диагностических сообщений. Ведь получится, что любой вызов функции в параллельной секции, неэкранированный конструкцией try/catch будет опасным. Хотя именно так оно и есть, но польза от такого количества диагностических сообщений сомнительна. Но возможно именно так себя в дальнейшем будет вести себя анализатор VivaMP в режиме "pedantic mode". И тогда безопасными, буду считать функции, явно помеченные, как не выбрасывающие исключений:

void MyNotThrowFoo() throw() { }

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

#pragma omp parallel num_threads (4)
{
    try {
        if (omp_get_thread_num ()  ==  0) {
            throw CException();
        }
        #pragma omp barrier
    }
    catch(CException &) {
    }
}

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

Дополнительная литература