Щелкните, чтобы оценить и отправить отзыв
 Параллельное выполнение. Средства и...
Related Articles

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

Richard Campbell and Kent Alstad

MSDN Magazine April 2008

...

Read more!

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

David Callahan

MSDN Magazine October 2008

...

Read more!

В этом месяце: проблемы доступа к памяти в многоядерных системах и диагностика и устранение ложного совместного доступа в приложениях параллельных вычислений.

Stephen Toub, Igor Ostrovsky, and Huseyin Yildiz

MSDN Magazine October 2008

...

Read more!

Мы кратко рассматриваем планируемую поддержку параллельного программирования в управляемом и машинном коде в следующей версии Visual Studio.

Stephen Toub and Hazim Shafi

MSDN Magazine October 2008

...

Read more!

В этой статье Джеффри Рихтер представляет свой класс AsyncEnumerator, который управляет итератором таким образом, что различные потоки пула потоков могут исполнять один и тот же код в различные моменты.

Джеффри Рихтер

MSDN Magazine Июнь 2008

...

Read more!

Popular Articles

В этой статье мы представляем методики программной и декларативной привязки данных и их отображения с помощью Windows Presentation Foundation.

Джош Смит

MSDN Magazine Июль 2008

...

Read more!

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

Гленн Блок (Glenn Block)

MSDN Magazine Сентябрь 2008

...

Read more!

Paul DiLascia

MSDN Magazine August 2002

...

Read more!

Мини-приложение для боковой панели является мощным небольшим инструментом, который удивительно легко создать. Займитесь этим вместе с Донаваном Уэстом.

Donavon West

MSDN Magazine August 2007

...

Read more!

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

Dan Griffin

MSDN Magazine May 2008

...

Read more!

Параллельное выполнение
Средства и приемы для выявления проблем параллельного выполнения
Рахул В. Патил и Бобби Джордж (Rahul V. Patil, Boby George)

В этой статье рассматриваются следующие вопросы.
  • Проблемы при тестировании параллельного выполнения.
  • Трудновыявляемые ошибки и их последствия.
  • Новые средства для выявления препятствий.
  • Рекомендации по проектированию тестов.
В данной статье использованы следующие технологии.
Динамический и статический анализ, проверка на основе моделей.
Чтобы удовлетворить постоянно растущие потребности в вычислительных ресурсах, производители оборудования все чаще делают выбор в пользу многоядерных процессоров. Если при использовании одноядерных процессоров повышение производительности достигается исключительно благодаря повышению тактовой частоты, то при использовании многоядерных систем успех зависит от того, насколько эффективно реализована параллельная обработка в приложении.
Отдельные формы параллелизма существуют в мире программного обеспечения уже давно. Однако для создания крупных приложений, использующих возможности многоядерного оборудования в полную силу, требуются иные методы разработки — не те, что применяются при разработке последовательных приложений.
Тестирование приложений, задействующих параллельную обработку, — задача непростая. В частности, ошибки параллелизма сложно выявить из-за недетерминированности поведения параллельных приложений. А если ошибка обнаружена, то ее сложно воспроизвести повторно. Кроме того, не так просто проверить, действительно ли была устранена ошибка или она была лишь замаскирована. Кроме того, при параллелизации могут возникать узкие места производительности, которые также нужно отслеживать.
В данной статье мы рассмотрим приемы тестирования параллельных приложений и представим шесть инструментов, полезных для определения потенциально серьезных дефектов. Начнем с ошибок, связанных с состояниями гонки, неправильным выполнением взаимоисключения и изменением порядка памяти.

Состояния гонки, взаимоблокировки и др.
Состояние гонки возникает тогда, когда несколько потоков многопоточного приложения пытаются одновременно получить доступ к данным, причем хотя бы один поток выполняет запись. Состояния гонки могут давать непредсказуемые результаты, и зачастую их сложно выявить. Иногда последствия состояния гонки проявляются только через большой промежуток времени и в совсем другой части приложения. Кроме того, ошибки такого рода невероятно сложно воспроизвести повторно. Для предотвращения состояния гонки используются приемы синхронизации, позволяющие правильно упорядочить операции, выполняемые разными потоками.
Определение данных для тестирования и критериев для отчета
В случае параллельных приложений тестовые данные определяются параметрами масштабируемости выбранного варианта использования данного приложения. Масштабируемость приложения в основном зависит от параллелизуемости примененного алгоритма. Некоторые алгоритмы допускают даже более чем линейное ускорение для отдельных задач (например, для поиска по неравномернму распределению). Другие алгоритмы такого увеличения скорости не дают из-за ограниченных возможностей параллелизации, и в результате суммарное увеличение скорости не пропорционально количеству ядер. Третьи алгоритмы (например, сортировки) вообще не дают линейного увеличения скорости по размеру набора данных. Как мы видим, очень важно понимать принципы масштабирования производительности приложения относительно следующих категорий.
<span class="ArticleInlineTitle">Количество потоков или ядер процессора</span> Обычно пользователь предполагает, что параллельное приложение при увеличении числа ядер или потоков масштабируется линейно.
<span class="ArticleInlineTitle">Размер набора данных</span> Обычно пользователь считает, что увеличение объема вводимых данных не должно снижать производительность приложения.
<span class="ArticleInlineTitle">Рабочая нагрузка</span>В ходе работы приложения объем работ, выполняемых отдельным потоком, может меняться. Он может увеличиваться, уменьшаться, меняться случайным образом или иметь нормальное распределение. Изменение загрузки потока в ходе выполнения задания может влиять на показатели производительности приложения. Рабочая нагрузка может быть следующих типов: постоянная, увеличивающаяся, уменьшающаяся, случайная и нормально распределенная.
Время выполнения — наиболее распространенный показатель, используемый в отчетах. Среди других показателей — увеличение скорости относительно последовательного приложения и увеличение скорости по различным категориям масштабируемости. Важно точно определить показатели и методы их измерения. Для начала нужно определить, какие значения будут включаться в отчет, каким образом, какие средства будут использоваться для определения влияния тех или иных файкторов на производительность. Измерения, если возможно, следует проводить по частям, чтобы определить производительность различных составляющих системы.
В ряде случаев гонка создается намеренно и ошибок не вызывает. Например, в приложении может использоваться глобальный маркер done, причем доступ к нему на запись имеет только один поток, а на чтение — несколько. Записывающий поток устанавливает маркер и тем самым заставляет остальные потоки корректно завершить работу. Потоки, выполняющие чтение, могут работать в цикле вида while (!done), постоянно считывая значение маркера. Как только поток обнаруживает, что маркер установлен, он выходит из цикла. В большинстве случаев такая гонка является безопасной. Далее в статье мы рассмотрим средства, используемые для выявления состояний гонки. Нужно отметить, что подобные средства могут выдавать предупреждение даже в случае безопасной гонки, давая так называемые ложные положительные результаты.
Взаимоблокировка возникает тогда, когда существует несколько потоков, и каждый из них ждет завершения работы другого: получается замкнутый круг, и в результате ни один поток не может продолжить работу. Взаимоблокировки иногда вводятся в программу самим разработчиком при попытке предотвратить возможные состояния гонки. Например, неправильное использование примитивов синхронизации, в частности неправильный порядок захвата блокировки, может привести к тому, что несколько потоков будут ждать друг друга. Впрочем, взаимоблокировки возникают и при отсутствии конструкций блокировки; причиной блокировки может стать любая разновидность циклического ожидания.
Рассмотрим пример ситуации, в которой может возникнуть взаимоблокировка. Поток 1 делает следующее:
Acquire Lock A;
Acquire Lock B; 
Release Lock B; 
Release Lock A;
Thread 2 proceeds like so:
Acquire Lock B;
Acquire Lock A; // if thread 1 has already acquired lock 
                // A and waiting to acquire lock B, then 
                // this is a deadlock
Release Lock A;
Release Lock B;
Голодание — это остановка работы одного или нескольких потоков многопоточного приложения на неопределенное время (или бессрочно). Потоки, которые не диспетчеризуются для выполнения, даже если они не блокируются и не ожидают чего-либо, называются испытывающими голодание. Причина голодания обычно кроется в правилах и политиках диспетчеризации. Например, если на одноядерном процессоре диспетчеризовать постоянно работающий неблокирующий поток с высоким приоритетом, то другой поток, с более низким приоритетом, работать не начнет никогда. Чтобы избежать подобных ситуаций, планировщик Windows® время от времени вмешивается в ход работы, повышая приоритет потоков, испытывающих голодание.
Активная блокировка возникает тогда, когда потоки запланированы, но не выполняют никаких операций, поскольку постоянно реагируют на изменение состояния другого потока. Лучшая иллюстрация активной блокировки — два человека, идущие навстречу в узком проходе, которые одновременно отступают в сторону, пытаясь пропустить друг друга. Каждый такой шаг в сторону не дает двигаться вперед — возникает активная блокировка. Классическим показателем активной блокировки является высокая загрузка процессора при отсутствии видимых результатов работы. Блокировки такого типа невероятно сложно выявлять и диагностировать.

Пробемы, связанные с определением порядка операций
В процессе оптимизации кода компилятор может переставлять его фрагменты местами для повышения производительности. Иногда это неожиданным образом сказывается на поведении потоков, отслеживающих изменения глобального состояния. Допустим, у нас есть два рабочих потока: поток 1 и поток 2. И есть две глобальные переменные, x и y, обе в начале работы программы имеют значение 0. Поток 1 работает следующим образом:
x=10;
y=5;
Поток 2 делает следующее:
if (y == 5)
{
  Assert(x != 10);
}
Компилятор, проводящий оптимизацию, заметит, что переменной y в любом случае будет присвоено значение 5, и переставит фрагменты кода таким образом, что y будет приравниваться к 5 до того, как переменной x будет присвоено значение 10. Если оба значения присваивает один и тот же поток, разницы нет. В результате возникает вероятность того, что будет вызвана операция assert, предусмотренная в потоке 2, поскольку переменная x может не равняться 10.
Изменения порядка операций можно при необходимости избежать, используя пересчитываемые переменные и встроенные элементы компилятора. Также нужно заметить, что если отключить общие маркеры оптимизации, поток 2 вообще никогда не обнаружит изменения переменных x и y. Здесь нужно использовать пересчитываемые переменные.
Изменение порядка операций компилятором не единственная сложность. Бывают ситуации, когда в результате оптимизации доступа к памяти (кэширования, усовершенствования загрузки путем упреждающего выполнения) поток, работающий на одном процессоре, может решить, что операции потока, работающего на другом процессоре, выполняются не в том порядке. Такое происходит исключительно в многопроцессорных или многоядерных системах.
Допустим, у нас есть два рабочих потока: поток 1 и поток 2. Есть две глобальные переменные (m и n), имеющие на начало работы программы значение 0, и локальные переменные a, b, c и d. Поток 1 (работающий на процессоре 1) выполняет следующие операции:
m = 5;     // A1
int a = m; // A2
int b = n; // A3
Поток 2 (работающий на процессоре 2) следующие:
n = 5;     //A4
int c = n; //A5
int d = m; //A6
Если мы исходим из условий последовательного выполнения, логично предположить, что ситуация b == d == 0 в результате выполнения всех инструкций возникнуть не может. Однако изменение порядка операций способно привести к тому, что операции записи в общую память (A1 и A4) будут перенаправляться буфером хранения, а значит, разные процессоры будут по-разному воспринимать эти операции записи.

Стратегии тестирования
В тестирование параллелльного приложения входят проверки правильности, стабильности, производительности и масштабируемости. Правильность работы определяется при помощи таких методов, как статический анализ, динамический анализ и проверка на основе моделей. Производительность и стабильность тестируются путем отслеживания влияния параллелизма и стрессовых тестов на объем нагрузки. Показателем масштабируемости является способность приложения работать на системах разного размера.
Статический анализ — это анализ кода без запуска приложения. Обычно при статическом анализе проверяются метаданные откомпилированного приложения или откомментированного исходного кода. Зачастую в него также включается некий этап формальной проверки, который должен гарантировать, что предположения, на которые опирался программист во время разработки, не приводят к неправильному поведению приложения. Из всех средств статического анализа приложений в машинных кодах наибольшую популярность имеют Prefix и Prefast. Для приложений в управляемом коде чаще всего применяется FxCop.
Статический анализ имеет как преимущества, так и недостатки. В качестве преимуществ можно назвать подробность и тщательность исследования, возможность убедиться в правильности проекта приложения, точность отчетов об ошибках, упрощающих поиск и устранение дефектов в программе. В другой стороны, статический анализ действительно помогает устранять дефекты только при наличии достаточного количества комментариев к коду. Опять же, комментарии должны быть верными. Кроме того, средства статического анализа дают большое количество ложных положительных результатов и требуют немалых усилий для борьбы с ними.
При динамическом анализе отслеживание дефектов ведется по результатам запуска приложения. Динамический анализ бывает двух типов: интерактивный и автономный. Средства интерактивного динамического анализа проверяют приложение непосредственно во время его работы; средства автономного динамического анализа фиксируют ход работы приложения, а затем проверяют записи, выявляя дефекты. Динамический анализ удобен тем, что он не требует практически никаких дополнительных усилий от разработчика на этапе создания кода, он дает меньше ложных положительных результатов и упрощает устранение наиболее очевидных проблем. Поскольку дефекты приложения определяются только в ходе работы приложения, первыми всплывают те неполадки, которые связаны с наболее часто выполняемым операциями. Такая особенность динамического анализа снижает затраты на повышение стабильности приложения.
Но и недостатки у динамического анализа тоже есть. Проверка происходит исключительно во время работы приложения, и процент покрытия кода при тестировании зависит от того, насколько хорошо написаны тестовые примеры. В действительности, некоторые средства способны зафиксировать состояние гонки, только если оно возникло в данном (проверяемом) сеансе работы программы. То есть, если средство анализа сообщает, что ошибок нет, вы все равно не можете быть уверены в этом. Еще один недостаток состоит в том, что большинство средств динамического анализа зависят от некоего инструментария, позволяющего менять поведение исполняющей среды. Из-за сложности таких средств производительность их не слишком высока.
Последним методом является проверка на основе моделей — метод проверки правильности работы конечного автомата, в котором применяется параллельная обработка. Этот метод допускает формальный дедуктивный анализ. Средство проверки моделей пытается смоделировать состояния гонки или взаимоблокировки. Проверка на основе моделей позволяет формально обосновать отсутствие этих дефектов. Такой метод помогает убедиться в правильности проекта и архитектуры приложения, полностью охватывает все его компоненты и требует минимального количества внешних ресурсов.
Как и другие методы тестирования, проверка на основе моделей имеет свои недостатки. В большинстве случаев очень сложно автоматически вывести модель из кода (есть случаи, когда это возможно, но они редки). Вывод модели вручную — занятие трудоемкое. Кроме того, при взрывном расширении пространства состояний (условии, при котором имеется слишком много возможных состояний автомата) объем проверки увеличивается до недопустимого. Расширение пространства состояний можно в некоторой степени контролировать, применяя методы сокращения, использующие сложную эвристику. Часть из них, впрочем, имеет оборотную сторону: они могут привести к тому, что некоторые дефекты будут пропущены.
Наконец, проверка на основе моделей требует наличия качественного плана проектирования и реализации. Она может подвердить правильность проекта, но это не гарантирует осутствия ошибок в реализации. На практике проверка на основе моделей оказывается действенной лишь для небольших, основных блоков приложения. Кроме упомянутых методов, существуют гибридные виды тестирования, сочетающие в себе динамический анализ и проверку на основе моделей. В качестве примера можно назвать средство CHESS, которое мы рассмотрим чуть ниже.
Алгоритмы выявления состояния гонки
Алгоритм блокирования, применяемый как в статическом, так и в динамическом анализе, выявляет потенциальные состояния гонки при одновременном доступе нескольких потоков к памяти без захвата общей блокировки. В общих чертах, алгоритм утверждает, что для любого фрагмента общей памяти v существует непустое множество блокировок C(v), которое будет нести любой поток во время доступа к переменной. Изначально C(v) представляет собой список всех блокировок. Кроме того, каждый поток поддерживает два набора блокировок: locks(t), соответствующий всем удерживаемым блокировкам, и writeLocks(t), соответствующим тем блокировкам, которые удерживаются для операций записи. Алгоритм устроен следующим образом:
  1. Для каждого фрагмента общей памяти v поддерживается набор блокировок C(v). Изначально C(v) представляет собой список всех блокировок.
  2. Кроме того, каждый поток поддерживает два набора блокировок: locks(t), соответствующий всем удерживаемым блокировкам, и writeLocks(t), соответствующим тем блокировкам, которые удерживаются для операций записи.
  3. При каждом чтении переменной v потоком t происходит следующее:
    1. C(v) присваивается пересечение C(v) и locks(t).
    2. Если C(v) == NULL, выдается ошибка.
  4. При каждой записи в переменную v потоком t происходит следующее:
    1. C(v) присваивается пересечение C(v) и writeLocks(t).
    2. Если C(v) == NULL, выдается ошибка.
По мере работы приложения множество C(v) для каждой переменной уменьшается. Ошибка выдается тогда, когда множество C(v) становится пустым (например, если пересечение наборов блокировок, удерживаемых потоками, пусто на момент обращения к общей памяти).
К сожалению, не все состояния гонки, выявляемые алгоритмом блокирования, являются таковыми. Код без состояний гонки можно создать, не используя блокировки. Вместо них могут применяться программные методы и другие примитивы синхронизации, к примеру сигнализация. Это крайне затрудняет поиск настоящих дефектов. Избежать подобных трудностей помогают комментарии и определнного рода запреты.
Еще один алгоритм выявления состояний гонки — так называемый алгоритм «предварительного события» (happens-before), основанный на частичном упорядочении событий в системах распределенных вычислений. Ниже приводится краткое описание алгоритма, вычисляющего частичный порядок и позволяющего определить, какое событие произошло перед указанным событием (в данном контексте события — это все машинные команды, в том числе операции четния/записи и блокировки).
  • В пределах одного потока события упорядочены по времени возникновения.
  • Между потоками события упорядочены на основании свойств примитивов синхронизации. Например, если блокировку lock(a) удерживают два потока, то считается, что снятие блокировки одним потоком было предварительным событием относительно захвата блокировки другим потоком.
  • Если несколько потоков осуществляют доступ к общей переменной, причем порядок доступа не определен принципом «предварительного события», считается, что возникает гонка.
Данный алгоритм приведен на рис. A.
Снятие блокировки потоком 1 является предварительным событием относительно захвата блокировки потоком 2. В результате доступ к общей переменной никогда не осуществляется одновременно, и состояние гонки не возникает. Недостатком этого алгоритма является то, что отслеживать подобные отношения весьма непросто.
Однако более значимая проблема состоит в том, что способность алгоритма выявлять состояния гонки полностью зависит от порядка диспетчеризации потоков. Частичный порядок строится только для данного конкретного экземпляра порядка, а в другой день могут возникнуть дефекты, которые в нем обнаружены не были. На рис. B состояний гонки выявлено не было, а вот при порядке выполнения, представленном на рис. C, конфликты будут. Бывало так, что состояния гонки выявлялись лишь через несколько лет после выпуска продукта. Таким образом, этот спомоб определения не может дать полной уверенности в том, что проверено все приложение.
С другой стороны, алгоритм предварительного события дает очень мало ложных положительных результатов. Большинство выявленных дефектов действительно являются дефектами. Но все же он пропускает многие ошибки, и эффективной работы этого алгоритма добиться достаточно сложно.
Алгоритм блокирования, напротив, весьма эффективен и выявляет большее количество ошибок, но дает слишком много ложных положительных результатов. Предпринимались попытки совместить эти два алгоритма, дабы избавиться от недостатков обоих подходов. Примечание: описанные алгоритмы выявления состояний гонки развивались в течение нескольких лет. Исходный код для них можно найти на порталах ACM /IEEE.
Рис. A Алгоритм предварительного события (щелкните изображение, чтобы увеличить его)
Рис. B Состояния гонки не обнаружены (щелкните изображение, чтобы увеличить его)
Рис. C Будут обнаружены состояния гонки (щелкните изображение, чтобы увеличить его)

Средства для тестирования параллельного выполнения
На рынке существует ряд средств для тестирования параллельного выполнения, помогающих устранять проблемы, связанные с взаимоблокировками, голоданием и прочеми сложностями, возникающими при выполнении параллельных операций. Каждое из средствв, которые мы здесь рассмотрим, полезно в своей отдельной области.
CHESS Средство CHESS, разработанное специалистами Майкрософт, сочетает в себе проверку на основе моделей и динамический анализ (см. go.microsoft.com/fwlink/?LinkId=116523). Оно выявляет ошибки параллельной обработки операций путем систематического исследования диспетчеризаций потоков и чередования. Это средство способно обнаруживать неполадки, связанные с состояниями гонки, взаимоблокироваками, зависанием, активными блокировками и повреждением данных. Оно также упрощает устранение неполадок, поскольку дает полностью воспроизводимые результаты. Как и в случае любой другой проверки на основе моделей, систематические исследования дают хорошее покрытие кода.
CHESS, будучи средством динамического анализа, последовательно выполняет обычный модульный тест циклически с помощью специализированного планировщика. При каждом повторе выбирается новый порядок диспетчеризации. С другой стороны, это средство проверки на основе моделей, и оно контролирует специализированнфый планировщик, способной создавать специальное чередование потоков. Для управления расширением пространства состояний в средстве CHESS применяется сокращение частичного порядка и органичением числа новых контекстов итераций.
Используя ограничение числа контекстов итераций, CHESS, не уменьшает глубину расширения пространства состояний, а ограничивает количество переключений потоков в данном сеансе работы приложения. Сам поток может выполнять любое количество действий между переключениями, причем глубина выполнения остается неограниченной (это большое достижение по сравнению с традиционной проверкой на основе моделей). Такой принцип работы основан на эмпирических данных о том, что для выявления большинства ошибок параллельной обработки достаточно небольшого числа систематических переключений потоков.
Средство CHESS позволяет выявлять взаимоблокировки и состояния гонки (применяется алгоритм блокирования Goldilocks, описанный на странице go.microsoft.com/fwlink/?LinkId=116877), но результаты его деятельность зависят от того, насколько качественно программист проверил другие состояния. Кроме того, средство изначально предполагает, что все программы должны завершить работу, и что существует гарантия равноправия всех потоков. То есть, если программа попадает в бесконечный цикл, средство сообщает о наличии активной блокировки.
Тестирование начинается с того, что CHESS запускается при границе числа контекстов итераций, равной 2. Как только будут выявлены все возможные ошибки, граница повышается до 3 и так далее. Опыт показывает, что большинство ошибок выявляется при значениях границы 2 и 3. Следовательно, таким образом можно сократить время поиска ошибок.
Что касается инструментирования, то CHESS обходит вызовы синхронизации интерфейса API Win32® для управления и намеренно вводит недетерминированность. Кроме того, это средство требует, чтобы в коде было довольно много утверждений, гарантирующих согласованность состояний (в грамотно написанном коде оно так и должно быть). Однако, как и большинство средств динамического анализа, CHESS обеспечивает хорошее покрытие приложения только при наличии качественного комплекта тестов. CHESS — отдушина для тех разработчиков и тестировщиков, которые полагаются при тестирвоании чередований только на стрессовые тесты. Это средство выполняет регулярные проверки модулей и методически моделирует нужные чередования.
Intel Thread Checker Это средство динамического анализа, позволяющее выявлять взаимоблокировки (в том числе и потенциальные), зависания, состояния гонки за данные и случаи некорректного использования интерфейсов API Windows для синхронизации (см. go.microsoft.com/fwlink/?LinkId=115727). Средство Thread Checker требует, чтобы в исходном коде или скомпилированном двоичном файле было инструментирование, позволяющее наблюдать каждую ссылку на область памяти и каждый стандартный примитив синхронизации Win32. Во время выполнения такой двоичный файл предоставляет анализатору достаточно информации для построения частичного порядка выполнения операций. После этого выполняется анализ по алгоритму предварительного события. Дополнительные сведения о нем приведены на врезке «Алгоритмы выявления состояний гонки».
Из соображений производительности и масштабируемости средство сохраняет информацию только о самых последних обращениях к общей переменной. Это помогает повысить эффективность средства при анализе приложений, выполняющих долгосрочные операции. Побочным эффектом такого подхода является пропуск части ошибок. Впрочем, возможно, важнее выявить большинство ошибок в долгосрочных приложениях, чем все ошибки в приложениях, выполняющихся очень короткий промежуток времени.
Единственный крупный недостаток данного средства состоит в том, что оно не может учитывать синхронизацию через взаимозаблокированные операции, например через операции, используемые в круговой блокировке. Впрочем, среди средств для тестирования параллельных приложений, использующих только стандартные примитивы синхронизации, это средство тестирования, пожалуй, поддерживается лучше всего.
<span class="ArticleInlineTitle">RacerX</span> Это средство статического анализа, зависящее от организации операций, которое используется для выявления состояний гонки и взаимоблокировок. Оно позволяет избежать необходимости скрупулезно комментировать весь исходный код. Единственная информация, необходимая ему, — это таблица, в которой указываются интерфейсы API, используемые для получения и освобождения блокировок. Там же можно указать атрибуты примитивов блокировки, таких как повторения, блокирование, повторные входы. Эта таблица обычно получается очень маленькой, не более 30 записей. Тем самым устраняется необходимость создания комментариев к коду, а в больших системах эта задача весьма трудоемка.
На первом этапе RacerX несколько раз проверяет каждый файл исходного кода и составляет график логики управления (Control Flow Graph, CFG). График CFG содержит информацию о вызовах функций, общей памяти, использовании указателей и другие данные. При построении графика средство также обращается к таблице примитивов синхронизации и помечает вызовы этих интерфейсов API.
Когда график CFG полностью построен, начинается фаза анализа, включающая проверку на предмет наличия состояний гонки и взаимоблокировок. Проверка графика CFG может занимать много времени, но для максимального сокращения этой операции применяются технологии сокращения и кэширования. После проверки контекстных потоков включается алгоритм блокирования, выявляющий потенциальные состояния гонки (сведения об алгоритме см. на врезке «Алгоритмы выявления конфликтов»). В ходе анализа взаимоблокировок для каждого захвата блокировки вычисляются циклы блокировки.
Конечная фаза предполагает завершающую обработку всех обнаруженных ошибок с целью их упорядочения по важности. Авторы программы немало сделали для того, чтобы максимально сократить количество ложных положительных результатов и повысить надежность выявления ошибок (что и следует ожидать от статического анализа). Результаты работы этого средства действительно впечатляют. Очевидна его практическая польза в области тестирования.
Chord Это средство статического анализа для Java, не зависящее от организации процессов, но контекстно-зависимое. Независимость от организации процессов позволяет ему быть более масштабируемым по сравнению с другими средствами статического анализа, но ценой снижения точности. Это средство также задействует особые примитивы синхронизации, доступные в Java. В нем использован чрезвычайно сложный алгоритм, для объяснения которого потербуется ввести большое количество понятий. (Дополнительные сведения о средстве Chord см. на стр. go.microsoft.com/fwlink/?LinkId=116526.)
KISS Это средство для проверки на основе моделей, разработанное специалистами Майкрософт для параллельных приложений, написанных на C. Поскольку в системах параллельной обработки расширение пространства состояний происходит очень быстро, KISS преобразует параллельное приложение на C в последовательное приложений и моделирует чередование. После этого выполняется собственно сам анализ с применением последовательного стредства проверки на основе моделей.
Приложение инструментируется операторами, позволяющими преобразовать параллельное приложение в последовательное, причем KISS берет на себя обязанность по контролю недетерминированности. Недетерминированное переключение контекстов происходит по тому же принципу, что и в средстве CHESS, описанном выше. Программист должен включить в код утверждения, проверяющие исходные положения о параллельности выполнения. Это средство не дает ложных положительных результатов. Данное средство является исследовательским прототипом, оно применялось группой по разработке драйверов для Windows, в которых в основном используется язык C (см. go.microsoft.com/fwlink/?LinkId=115723).
Zing Это средство проверки на основе моделей, предназначенное для тестирования проектов паралельных приложений. В средстве Zing есть свой собственный язык, применяемый для описания сложных состояний и переходов. Это средство имеет все необходимое для моделирования параллельных конечных автоматов. Как и другие средства проверки на основе моделей, Zing представляет собой полноценное решение для проверки проектов приложений; оно также помогает убедиться в качестве проекта, поскольку позволяет проверить исходные положения и формально доказать наличие или отсутствие тех или иных условий. Это средство также позволяет контролировать расширение пространства состояний, применяя современные приемы редукции.
Модель, используемая в средстве Zing для проверки правильности программы, должна создаваться либо трансляторами, либо вручную. Трансляторы для отдельных областей написать можно, а вот полного и эффективного преобразователя для приложений в машинном коде и приложений CLR пока нет. По этой причине Zing вряд ли можно использовать в крупных проектах — разве что для проверки правильности отдельных блоков (см. go.microsoft.com/fwlink/?LinkId=115725.)

Тестирование производительности
Тестирование производительности — неотъемлемая и основополагающая часть процесса тестирования параллельных приложений. В конце концов, параллельные приложения и создавались для того, чтобы обеспечить большую по сравнению с последовательными приложениями производительность. Как постулируется в законе Амдала и законе Густафсона, повышение производительность за счет введения параллельной обработки в значительной степени зависит от возможностей параллелизации использованных алгоритмов, количества фрагментов последовательной обработки, ресурсоемкости параллелизации и характеристик данных и рабочей нагрузки. Тестирование производительность позволяет участникам процесса оценить и проанализировать эффективность параллельного приложения.
Методика тестирования производительность параллельных приложений в общих чертах совпадает с методикой, применяемой для приложений последовательных. Далее мы опишем основные этапы тестирования производительности, требующие некоторой адаптации для параллельных приложений. Основные факторы, которые необходимо учесть, описаны на врезке «Определение тестовых данных и составление отчетов по показателям».
Самый важный этап тестирования производительности — определение целей и задач. Для параллельных приложений в качестве задач можно перечислить оценку параллелизации и масштабируемости используемого алгоритма, оценку характеристик производительности различных вариантов проекта приложения, выявление дополнительных расходов ресурсов на синхронизацию и обмен данными, проверку соблюдения требований к производительности.
Следует отметить, что задачи и объемы тестирования зависят также от того, кто выполняет тестирование. Например, клиент, разворачивающий приложение, будет проводить проверку на предмет соответствия приложения задачам, стоящим перед его компанией, а разработчики будут заинтересованы в проведении широкого тестирования производительности, позволяющего выявить узкие места и расширить возможности параллелизации программы. Задачи тестирования зависят и от особенностей цикла разработки программного обеспечения. На этапах проектирования и реализации основной задачей будет оценка и определений способов повышения производительности приложения, тогда как на этапах тестирования и стабилизации в первую очередь нужно будет гарантировать отсутствие падения производительности от сборки к сборке.
Следующий важный шаг — определение схем тестирования и установка целевых показателей. Разработчики, заинтересованные в правильной оценке производительности параллельного приложения, должны предусмотреть три типа схем тестирования: схему для клиентов, тестирование по ключевым показателям производительности и микротесты производительности.
Эти три типа схем тестирования соотносятся друг с другом примерно так же, как системные тесты, тестирование интеграции и тестирование модулей. Другими словами, схема для клиентов включает в себя набор тестов на основе ключевых показателей производительности, а они, в свою очередь, включают набор микротестов. Знание отношений между этими тремя типами тестов позволяет разработчику понять, как блоки параллельного приложения влияют на производительность друг друга. Это дает возможность правильно определить серьезность проблем производительности и, что еще важнее, помогает разработчикам эффективно решать те проблемы, с которыми обращаются к ним клиенты.
Определить целевые показатели производительности для нетривиального приложения непросто, не говоря уже о параллельном приложении. Один из подходов состоит в том, чтобы выводить целевые значения путем триангуляции показателей, выведенных из ожидаемых или теоретических значений производительности для каждого компонента приложения, путем сравнительного анализа (в сравнении могут участвовать, например, аналогичные приложения фирм-конкурентов) и путем профилирования существующей реализации приложения (если таковая существует).
Для каждой схемы тестирования нужно также определить набор критериев допустимости (набор значений, при которых результаты теста считаются приемлемыми). При тестировании параллельного приложения в роли критериев допустимости могут выступать следующие переменные.
Разброс результатов тестирования Это показатель нестабильности приложения. Разброс можно оценить путем вычисления стандартного отклонения по множеству результатов.
Загрузка центрального процессора Низкая загрузка процессора может свидетельствовать о наличии проблем параллельной обработки, таких как взаимоблокировки и избыточное расходование ресурсов на синхронизацию. И наоборот, высокая загрузка процессора не обязательно говорит об отсутствии ошибок: она может быть связана с наличием активных блокировок.
Сборка мусора В управляемых приложениях избыток операций по обслуживанию памяти может помешать работе приложения, что говорит о дефектности его проекта или реализации.
Общее время работы потока Этот показатель позволяет примерно определить, сколько времени ушло бы на выполнение тех же операций в последовательном приложении. Разница между общим временем работы потока и временем, ушедшим на последовательное выполнение операций того же алгоритма, дает возможность оценить объем ресурсов, уходящих на параллелизацию (и определить правильность теста).
Общее расходование памяти Измерение общего расходования памяти может пригодиться для понимания схемы использования памяти приложением. В управляемых приложениях сборщик мусора играет важную роль при изменении общего объема расходуемой памяти, и при анализе использования памяти его нужно принимать в расчет.
Перед началом тестирования производительности необходимо выполнить три важных действия, помогающих получить более значимые результаты. Во-первых, нужно свести к минимуму разброс тестовой среды (вариативность может привноситься как самой средой, так и приложением). Для снижения разброса можно отключить ненужные службы и приложения, сократить взаимодействие сетей. Если разброс привносится приложением, можно выполнить несколько начальных итераций и собрать показатели, полученные от нескольких повторений теста.
Во-вторых, если в управляемом приложении не вовремя запустить сборщик мусора, результаты тестирования производительности могут оказаться неверными. То есть, нужно свести к минимуму вероятность включения сборщика мусора. Для этого сборщик мусора принудительно вызывается перед получением показателей или после него. Также можно изменить значение параметра GCLatencyMode на LowLatency (этот метод подходит только для Microsoft® .NET Framework 3.5).
В-третьих, перед сбором показателей нужно обязательно выполнять подготовку приложения. Если сначала выполнить пробный прогон теста, вы сможете избавиться от влияния таких разовых факторов, как инициализация переменных, динамическая компиляция (в управляемых приложениях), потери кэша. Но помните, что в некоторых случаях важно также тестировать производительность при холодном пуске. (Дополнительные сведения о повышении стартовой производительности приложения приведены в статье msdn2.microsoft.com/magazine/cc337892.aspx.)
Дополнительные материалы по тестированию параллельных приложений

Использование синхронизации

Охотник за ошибками. Wait Chain Traversal

Скажем «Нет!» зависаниям. Методы диагностики и устранения взаимоблокировок в приложениях .NET

ReadWriteBarrier (C++)

Упорядочение памяти в архитектуре Intel 64

Проблемы, связанные с синхронизацией и многопроцессорной обработкой

Поэтапное руководство по PreFast

Тестирование приложений с использованием AppVerifier

Руководство по тестированию производительности веб-приложений

Стрессовые тесты
Стрессовые тесты чаще всего предполагают проверку приложения на устойчивость, доступность и способность обработки ошибок без сбоев. Устойчивость обычно тестируется путем повторного вызова одних и тех же функций или их сочетаний с последующей проверкой правильности выполнения. Доступность тестируется путем запуска приложения при ограниченном количестве ресурсов, например при небольшом количестве памяти или высокой загрузке процессора. Приложение не должно завершаться аварийно или, по крайней мере, должно вести себя корректно. Корректность обработки ошибок проверяется методом искусственного вызова сбоев (внесения ошибок) или методом вызова многочисленных ошибок.
При проведении стрессового теста параллельного приложения особое внимание следует уделять устойчивости и доступности. Из-за недетерминированности поведения параллельных приложений последовательный вызов одних и тех же функций или сочетаний функций может привести к использованию различных веток кода и, следовательно, выявить большее количество ошибок. Тестирование доступности при ограниченных ресурсах (например, создание нескольких потоков, формирование ситуаций взаимоблокировки или активной блокировки) особенно важно, поскольку для параллельных приложений вероятность того, что клиент столкнется с подобными условиями, довольно высока.
Следует отметить, что анализ первоначальных причин ошибок параллельной обработки, выявляемых при стрессовых тестах, выполнить довольно сложно. Но поскольку на сегодняшний день средства тестирования параллельных приложений все еще находятся в зачаточном состоянии, стрессовые тесты оказываются отличным подспорьем для функционального тестирования.

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

Рахул В. Патил (Rahul V. Patil) является старшим разработчиком группы платформ параллельных вычислений в корпорации Майкрософт. Он отвечает руководство проектом тестирования для платформы параллельных вычислений в машинном коде.

Бобби Джордж (Boby George) также является старшим разработчиком группы платформ параллельной обработки в корпорации Майкрософт. Он отвечает за проект по тестированию производительности для платформы параллельных вычислений в управляенмолм коде.

Page view tracker