Мы продолжаем развивать наш статический анализатор 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, в результате чего возникнет зависание. И остальные потоки будут вечно ждать поток, в котором произошло исключение. Возможно, в следующей версии анализатора будет добавлен поиск соответствующих ошибок, но пока с этим связаны определенные технические сложности.
Дополнительная литература