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

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

Отладка последовательной программы основана на том, что степень предсказуемости начального и текущего состояний программы определяется входными данными. Когда программист переходит к отладке многопоточного кода, то он обычно сталкивается с совершенно уникальными проблемами: в различных операционных системах применяются разные стратегии планирования, нагрузка на вычислительную систему динамически изменяется, приоритеты процессов могут различаться и т. д. Так, точное воссоздание программы в некоторый момент ее выполнения (тривиальная задача для последовательной отладки) значительно усложняется при переходе к параллельной программе, что связано с недетерминированным поведением последней. Иными словами, поведение запущенных в системе процессов, а именно их выполнение и ожидание выполнения, взаимные блокировки и проч., зависит от случайных событий, происходящих в системе. Как же быть? Очевидно, для отладки параллельного кода требуются совершенно другие средства.

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

Рассмотрим несколько таких средств, предположив, что будем отлаживать OpenMP-программу, созданную в среде Microsoft Visual Studio 2005. Более подробно о стандарте OpenMP см. «Мир ПК», №10/07, с. 60.

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

 

Эта простая программа вычисляет значения некоторой функции. Ее можно легко распараллелить с помощью средств стандарта OpenMP. Добавим одну строку перед первым оператором for (листинг 2).

Данная программа успешно компилируется в среде MS Visual Studio 2005, причем компилятор даже не выдает никаких предупреждений. Однако она совершенно некорректна. Чтобы это понять, надо вспомнить, что в OpenMP-программах переменные делятся на общие (shared), существующие в одном экземпляре и доступные всем потокам, и частные (private), локализованные в конкретном процессе. Кроме того, есть правило, гласящее, что по умолчанию все переменные в параллельных регионах OpenMP общие, за исключением индексов параллельных циклов и переменных, объявленных внутри этих параллельных регионов.

В приведенном выше примере видно, что переменные x, y и s — общие, что совершенно неправильно. Переменная s обязательно должна быть общей, так как в рассматриваемом алгоритме она является, по сути, сумматором. Однако при работе с переменными x или y каждый процесс вычисляет очередное их значение и записывает в соответствующую из них. И тогда результат вычислений зависит от того, в какой последовательности выполнялись параллельные потоки. Иначе говоря, если первый поток вычислит значение для x, запишет его в переменную x, а потом такие же действия произведет второй поток, то при попытке прочитать значение переменной x первым потоком он получит то значение, которые было записано туда последним по времени, а значит, вычисленное вторым потоком. Подобные ошибки в случае, когда работа программы зависит от порядка выполнения различных фрагментов кода, называются race condition или data race (состояние «гонки» или «гонки» вычислительных потоков; подразумевается, что имеют место несинхронизированные обращения к памяти).

Для поиска таких ошибок необходимы специальные программные средства. Одно из них — Intel Thread Checker. Данная программа поставляется как модуль к профилировщику Intel VTune Performance Analyzer, дополняя имеющиеся средства для работы с многопоточным кодом. Intel Thread Checker позволяет обнаружить как описанные выше ошибки, так и многие другие, например deadlocks («тупики», места взаимной блокировки вычислительных нитей) и утечки памяти.

После установки Intel Thread Checker в диалоге New Project приложения Intel VTune Performance Analyzer появится новая категория проектов — Threading Wizards (мастера для работы с потоками), среди которых будет Intel Thread Checker Wizard. Необходимо выбрать его, а в следующем окне мастера указать путь к запускаемой программе. После запуска программа начнет выполняться, а профилировщик соберет все сведения о работе приложения. Пример такой информации, выдаваемой Intel Thread Checker, приведен на рис. 1.

Как видно, даже для такой небольшой программы количество ошибок достаточно велико. Thread Checker группирует обнаруженные ошибки, одновременно оценивая их критичность для работы программы, и приводит их описание, что существенно повышает эффективность работы программиста. Кроме того, на вкладке Source View представлен программный код приложения с указанием тех мест в коде, где имеются ошибки (рис. 2).

Итак, описанную выше и обнаруженную средствами Intel Thread Checker ошибку записи в переменные x и y исправить довольно просто: нужно лишь добавить в конструкцию #pragma omp parallel for еще одну директиву: private (x, y). Таким образом, эти две переменные будут объявлены как частные, и в каждом вычислительном потоке будут свои копии x и y. Следует также обратить внимание, что все потоки сохраняют вычисленный результат добавлением его к переменной s. И здесь кроется ошибка, для которой Thread Checker приводит такое описание — Write‡Read data race (состояние «гонки» вычислительных потоков). Подобные ошибки происходят тогда, когда один вычислительный поток пытается записать некоторое значение в общую память, а другой в то же время выполняет операцию чтения. В рассматриваемом примере это может привести к некорректному результату.

Рассмотрим инструкцию s = s + j*y. Изначально предполагается, что каждый поток суммирует вычисленный результат с текущим значением переменной s, а потом такие же действия выполняют остальные потоки. Однако возможна ситуация, когда, например, два потока одновременно начали выполнять инструкцию s = s + j*y, т. е. каждый из них сначала прочитает текущее значение переменной s, затем прибавит к этому значению результат умножения j*y и полученное запишет в общую переменную s.

В отличие от операции чтения, которая может быть реализована параллельно и является достаточно быстрой, операция записи всегда последовательна. Следовательно, если сначала первый поток записал новое значение, то второй поток, выполнив после этого запись, затрет результат вычислений первого, потому что оба вычислительных потока сначала прочитали одно и то же значение s, а потом стали записывать свои данные в эту переменную. Иными словами, то значение s, которое второй поток в итоге запишет в общую память, никак не учитывает результат вычислений, полученный в первом потоке. Можно избежать подобной ситуации, если гарантировать, что в любой момент времени операцию s = s + j*y разрешается выполнять только одному из потоков. Такие операции называются неделимыми или атомарными. Когда нужно указать компилятору, что какая-либо инструкция является атомарной, используется конструкция #pragma omp atomic. Программный код, в котором исправлены указанные ошибки, приведен в листинге 3.

 

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

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

Отладчик Intel Thread Checker поддерживает анализ 32- и 64-разрядных приложений в операционных системах MS Windows и Linux. Среди других средств отладки для параллельных приложений можно выделить широко распространенный отладчик TotalView, разрабатываемый компанией TotalView Technologies, а также отладчик dbx и утилиту Thread Analyzer из пакета Sun Studio компании Sun Microsystems. Хотя TotalView и является коммерческим продуктом, он доступен для операционных систем Linux, UNIX, Mac OS, поддерживает языки Си, С++, Фортран и технологии параллельного программирования OpenMP и MPI (Message Passing Interface).

Компилятор Sun Studio имеет поддержку OpenMP, а с помощью отладчика dbx можно выполнять код в области параллельности в пошаговом режиме, устанавливать в параллельном коде точки останова, контролировать значения переменных, которые определены как частные, и т. д. Указанная выше утилита для анализа многопоточного кода Thread Analyzer во многом схожа с Intel Thread Checker: ее работа также строится по принципу сбора данных для дальнейшего анализа. Она может применяться для поиска конфликтов доступа к данным (data races) и тупиков (deadlocks). Для использования Thread Analyzer программа должна быть собрана со специальным ключом компилятора Sun Studio.

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