№ 4(34) 2011
Н. В. Заборовский, аспирант Московского физико-технического института
(государственного университета)
А. Г. Тормасов, докт. физ.-мат. наук, профессор Московского физико-технического
института (государственного университета)
Моделирование многопоточного исполнения программы и метод статического анализа кода на предмет состояний гонки
Существуют различные методики анализа корректности многопоточных алгоритмов. Статья посвящена рассмотрению данного вопроса в контексте состояний гонки. Предлагаемый автором подход ориентирован на задачи со сложными конструкциями кода.
Введение
Состояние гонки (race condition) — ситуация, когда несколько потоков одновременно обращаются к одному и тому же ресурсу, причем хотя бы один из потоков выполняет операцию записи, и порядок этих обращений точно не определен. Обнаружить наличие состояния гонки в коде очень непросто, так как о них можно и вовсе не знать, пока они не проявят себя. Процедура обнаружения состояния гонки может быть очень трудоемкой, даже если в наличии есть состояния всех потоков исполнения на момент некорректной ситуации.
Обзор существующих моделей
В настоящее время известны многие работы, посвященные моделированию исполнения многопоточных программ. Так, например, в [1] описана процедура построения модели исполнения многопоточного алгоритма на разделяемой памяти с целью определения состояния гонки. Приведем краткое описание модели. В ее основе лежит граф совместного исполнения потоков G: = (V, A), в котором путям соответствуют всевозможные варианты исполнения многопоточного алгоритма. Вершинам графа V соответствует множество состояний общей памяти по-
сле выполнения очередной атомарной инструкции, дугам А — атомарные операции над разделяемыми переменными. Каждому ребру поставлена в соответствие операция (Я — чтение, № — запись и X — другая) и ячейка памяти, над которой производится операция. Также для модели определена функция корректности, отражающая субъективное понятие о корректном или некорректном исполнении программы. Одна и та же многопоточная программа может быть корректной с точки зрения одной
Рис. 1. Граф совместного исполнения двух потоков на разделяемой памяти
1 105
-ч ПРИКЛАДНАЯ ИНФОРМАТИКА
№ 4 (34) 2011 ' -
i is
1 и
U
0
U к
и
1 §
<0
if S
1 §
<0
Si £
Й is
u
Si
I
I
0 &
IE
1
Si
о
0 с
is
IS
1
s
О
! 5
функции корректности и некорректной — с другой. О процедуре анализа на основании описанной модели скажем ниже. Отметим, что граф G имеет вид ромба, в котором начальная вершина находится вверху, конечная — внизу, а все ребра имеют направление сверху вниз.
Сильное место данного подхода — малая сложность анализа. Количество вершин на каждом из ребер ромба линейно зависит от количества операций, и каждая вершина проходится при анализе только один раз, вследствие чего сложность подхода представляет собой О (кхп), где к и п — количество операций каждого из потоков.
Немного о сути анализа. Рассмотрим путь на графе из начальной вершины в конечную. При проходе обращаем внимание на то, что два ребра, исходящие из одной вершины, могут оперировать разными ячейками памяти или только читать одну и ту же ячейку. Такие операции называем не коммутирующими и обозначаем их крестом. То, что две операции разных потоков являются не коммутирующими, говорит о том, что результат алгоритма будет зависеть от порядка их исполнения. В случае если операции коммутируют, для анализа не важен порядок исполнения, поэтому из вершины можно двигаться по любому из исходящих ребер — на состояние памяти в финальном состоянии это не повлияет. На данном наблюдении основывается построение классов эквивалентности — наборы полных путей на графе G, для которых содержимое ячеек памяти в финальном состоянии одинаково. После нахождения всех классов путей достаточно взять по одному представителю из каждого класса, чтобы перебрать все возможные последовательности смены ячеек памяти.
Вопрос о наличии состояния гонки решается следующим образом. Гонка (в терминах графа G) — это существование двух разных полных путей, для которых в финальном состоянии значения хотя бы одной из ячеек памяти различны. С учетом начальных значений ячеек и того, как их значения меняются, используем метод неопределенных
коэффициентов. На каждом ветвлении добавляем коэффициент а к изменению в левой ветви и (1 - а) — в правой. В финальной вершине имеем множество значений, где каждая ячейка памяти в общем случае имеет вид:
Xj = f (X0,{a}),
где x0 — состояние всех ячеек памяти в начальной вершине, {а} — значения неопределенных коэффициентов. Исходя из определения понятия гонки и принципов построения и анализа графа, изложенных выше, получаем, что гонка возможна тогда и только тогда, когда
э /, {aj^a^ : f (Xo,{a^) ф f (Xo,{ay.
Слабое место подхода заключается в том, что в нем отсутствует привязка к конкретным значениям переменных. В терминах графа G это означает, что, если в графе есть недостижимые дуги, алгоритм не сможет сделать вывод об их недостижимости. Привязка к значениям переменных — критический момент в анализе многопоточных алгоритмов, потому что логика непопадания в критические секции и поведение алгоритмов в целом базируется в реальных задачах именно на значениях переменных. Иначе говоря, в рассмотренном подходе не предусмотрен анализ условных конструкций типа if-else и условий внутри циклов for, while, do-while. Кроме того, нет формальной процедуры анализа алгоритмов, содержащих циклы.
Понять проблемы подхода и наметить пути решения поможет анализ конкретного алгоритма. Для этой цели возьмем алгоритм взаимного исключения Петерсона (листинг 1).
Начальное состояние всех трех ячеек памяти — 0. Некорректное поведение — одновременное нахождение обоих потоков в критической секции. Проанализируем эту задачу с помощью подхода, изложенного в [1]. Значения переменных в момент нахождения в критической секции одного потока не допускают в критическую секцию другой, и встает вопрос достижимости дуги в зависимости от значений переменных. Необходима
№ 4(34) 2011
Листинг 1
Алгоритм взаимного исключения Петерсона
Первый поток: Второй поток:
A1 ready [0] =1; B1 ready [1] =1;
A2 turn=1; B2 turn=0;
A3 while ( ready [1] && turn==1); B3 while ( ready [0] && turn==0);
A4 // Критическая секция B4 // Критическая секция
A5 ready [0] =0; B5 ready [1] =0;
со
0
л
1 Eg
о
t .S со
со
эй
формальная процедура, позволяющая определить, достижимо ли ребро. На построенном графе ситуации «попадание обоих потоков в критическую секцию» отвечает вполне конкретная область: A4-B4. Формально в [1] не описано никакой процедуры, определяющей достижимость того или иного ребра, поэтому ответа на вопрос о корректности алгоритма Петерсона подход не дает.
В данной работе предлагается модельный подход, позволяющий не только учитывать попарное сравнение операций потоков, но и анализировать поведение системы в целом, включая определение достижимости ребер и диапазонов значений переменных.
Методика построения расчетного графа
Общая идея подхода к задаче о нахождении состояния гонки
В качестве исходного материала для анализа служит программный код, исполняемый в нескольких потоках. На основе кода строятся специальные графы, вершинам и ребрам которых соответствует необходимая для анализа информация. Используется два вида графов: граф совместного исполнения потоков и расчетный граф. На основании анализа графа совместного исполнения выбираются пути, для которых возможно состояние гонки. На расчетном графе анализируются только эти пути и выводится результат о наличии состояния гонки.
Построение графа совместного исполнения п потоков
Покажем, как строится расчетный граф для двух потоков. Имеем начальную вершину, в которой состояние системы определя-
ется значениями, которыми проинициали-зированы переменные. Множество ребер A будем обозначать как а'1, где I — номер операции первого потока, а \ — второго. Тогда каждой вершине можно присвоить два индекса, означающие количество операций, сделанных каждым из потоков, чтобы оказаться в этой вершине. Из каждой вершины V, считая от начальной, будет исходить два ребра: а'+ и а'+1 до тех пор, пока в одном из потоков не закончатся операции — тогда из вершины будет исходить только одно ребро, соответствующее операции другого потока. Построенный таким образом граф, как уже было сказано, имеет вид ромба (см. рис. 1). Метод построения обобщается на случай п > 2 потоков: вершины и ребра будут иметь п индексов, а из каждой вершины будет исходить не менее п ребер. В общем случае граф совместного исполнения п потоков будет иметь вид п-мерного ромбовидного графа. Подробное описание примеров и доказательство корректности представления кода в виде графа совместного исполнения потоков дано в [1].
Общее представление о расчетном графе Расчетный граф — конструктивная дискретная модель исходного кода программы, представляющая собой направленный граф, в котором каждому ребру соответствует атомарная операция, а вершинам ставится в соответствие множество значений всех разделяемых переменных. В графе выделена начальная и конечная вершины, соответствующие начальному состоянию переменных перед исполнением и конечному — после исполнения. Проход по графу моделирует один из вариантов совместного исполнения
107
-N ПРИКЛАДНАЯ ИНФОРМАТИКА
№ 4 (34) 2011 ' -
i 12
1
il U
0
U к
1 §
!
i §
<0 ¡2
Si £
iï IS
u
Si «
I s
0 &
IE
1 §
¡2 о
0 с
¡2
1 I
S
О
! S
нескольких потоков путем точного указания следования инструкций одна за другой.
Множество значений разделяемых переменных будем называть состоянием системы. Вектор, компонентами которого являются значения разделяемых переменных, назовем вектором состояния. На расчетном графе определим функцию, соотносящую каждой вершине множество значений разделяемых переменных и функцию, соответствующую операции, производимой над системой на каждом из ребер:
S : V ^ х = (x1.....xn)
F : E ^ f(x), (3)
где x — состояние системы, а f — вектор-функция, показывающая, как меняется состояние системы после прохода по ребру.
Определим на этом графе также функцию условного перехода:
M :(vj,vj+1) ^ P(x) = 0,
где P — функция, определенная на множестве значений вектора разделяемых переменных. Это предикат, определяющий возможность передвижения системы из текущей вершины по рассматриваемому ребру, что соответствует условным переходам в программном коде. Циклы будем рассматривать в унифицированной форме, которая определяется ниже. Всякий цикл c предусловием может быть приведен к виду
for (initialization; condition; iteration) { operation;
Для унификации и применения одного формального подхода ко всем циклам с предусловием (while, for) преобразуем конструкцию цикла к следующему виду:
<initialization>; for(;;) {
if (!<condition>) break; <iteration>; <operation>;
Участки кода initialization, condition, iteration и operation будем заключать в угловые скобки ("<initialization>") и называть секциями, подразумевая, что в них содержится определенное количество операций. Для конструкций с постусловием (do-while) форма представления и дальнейшие рассуждения — аналогичные.
Обозначение на рисунках и схемах
Значения всех разделяемых переменных будем обозначать на £рафе в фигурных скобках: {0, 1}. Если F( x ) меняет значение переменной, то в вершине, куда ребро ведет, новое значение будет подчеркнуто: {0, !}. Каждой атомарной операции соответствует одно ребро (так же, как и в графе совместного исполнения потоков). Такие ребра входят, например, в состав секций циклов <initialization>, <iteration> и <operation>. Отдельно можно выделить специальный вид ребер — ребра условия. Они содержат пустую операцию над переменными: F = 1 и функцию-предикат P( x), определяемую секцией <condition> цикла (или содержимым обычного if). Такие ребра появляются, когда в программном коде есть ветвления. Условие прохода по ребру определяет достижимость вершин, куда ребро ведет.
Метод неопределенных коэффициентов
Различным путям графа по построению соответствуют различные варианты исполнения. Находясь в определенной вершине, система может перейти по любому одному из ребер в следующее состояние, что соответствует выполнению инструкции одним из потоков. Для описания ситуации используются неопределенные коэффициенты. Пусть для двух потоков система выполнит инструкцию первого потока с вероятностью а, второго — (1 - а). Если в первом потоке с разделяемой переменной совершается операция инкрементации, а во втором — прибавление числа 2, то результатом выполнения такой конструкции будет
а + 2 (1 - а) = 2 - а, где а = {0, 1}.
№ 4(34) 2011
Представление циклов и ветвлений
Линейный программный код отображается на ребро графа без каких-либо дополнительных действий. Операции чтения соответствует ребро с F = 1 и, возможно, нетривиальным предикатом Р(х), если речь идет, например, о конструкции if (х == 1). При появлении нелинейных участков, таких, как циклы и ветвления, анализ усложняется. Представление ветвлений использует неопределенные коэффициенты в и подобно коэффициентам а. Каждой из ветвей приписывается свой коэффициент, и только один из них равен 1, остальные — 0. На выходе имеем выражение с неопределенными коэффициентами, представляющее собой состояние системы на выходе из ветвления. Коэффициенты в выражении определяют ветвь, для которой моделируется исполнение. Обратных ребер в расчетном графе нет. Вместо них у ребер, входящих в тело цикла <^егайоп><орегайоп>, появляется зависимость от параметра — номера итерации в цикле. Не всегда зависимость от номера итерации может быть задана явно, однако для реальных задач она, как правило, именно явная. Дополнительное условие налагается на систему в секции <соп-dition>. Исходный цикл приобретает форму нескольких однонаправленных ребер, представляющих тело цикла, что, безусловно, облегчает задачу анализа.
Конструктивное построение расчетного графа
Построение расчетного графа аналогично построению графа совместного исполнения потоков. Как и в случае расчетного графа, вершины представляют собой состояния системы, отличающиеся друг от друга на одну атомарную операцию чтения/записи, а ребра соответствуют самим атомарным операциям. Множество ребер А будем обозначать как а', где I — номер операции первого потока, а \ — второго. Тогда каждой вершине можно присвоить два индекса, означающие количество операций, сделанных каждым из потоков, чтобы оказаться в этой вершине. Из каждой вершины V, счи-
тая от начальной, будет исходить два ребра: а'+1 и а'++1 до тех пор, пока у одного из потока не закончатся операции — тогда из вершины будет исходить только одно ребро, соответствующее операции другого потока. Построенный таким образом граф также имеет вид ромба. Отличия от графа совместного исполнения потоков заключаются в данных, сопоставляемых вершинам и ребрам. Каждому ребру соотносят функции (4) и (5). Для определенности будем подразделять ребра на два вида: ребра условия, где Р — нетривиальна, f — тривиальна и ребра операций, где Р — тривиальна, f — нетривиальна. Проход по такому графу из начальной вершины в конечную однозначно определяет последовательность взаимного выполнения инструкций двух потоков. Построение расчетного графа для п потоков полностью аналогично.
Расчеты на графе
После того как расчетный граф, состоящий из ребер операций и ребер условий, построен и начальной вершине соотнесено множество исходных значений разделяемый переменных, можно приступать к расчетам. Каждая из итераций — проход по одному из путей из начальной вершины в конечную. В общем случае число путей достаточ-
но велико —
п к
Однако будем рассматри-
вать только те пути, которые были отобраны с помощью классов эквивалентности на графе совместного исполнения потоков. Таких путей значительно меньше — О (п х к). Элементарная часть итерации — проход их текущей вершины по ребру в следующую в соответствии с выбранным путем и затем либо изменение вектора состояния, либо проверка условия достижимости и проход по ребру (или признание ребра недостижимым). Если в графе были ветвления, то ребра, соответствующие ветвям цикла, будут иметь коэффициент в и (1 - в). Коэффициенты войдут в выражение для значений переменных в финальной вершине. В зависимости от ро-
1 *
£
о
.Э со
со
Эй
109
№ 4 (34) 2011
{ООО}
{ООО}
Рис. 2. Расчетный граф для алгоритма Петерсона
| да задачи и функции корректности по графу делается вывод о недостижимости критиче-
| ской секции, присутствии состояния гонки
§ для конкретной разделяемой переменной
£ и других подобных условий.
1 Рассмотрим анализ алгоритма Петер-сона в качестве иллюстрации применения
¡о предлагаемого подхода (рис.2). Внутренние
« ребра намеренно не подписаны, но подра-
§ зумевается, что расчеты производятся и для них тоже. В фигурных скобках обозначены
§ значения разделяемых переменных. Жир-
2 ным выделены и подчеркнуты те значения, § которые изменились при переходе по реб-| ру а также начальные значения переменных. 2 Также подписаны ребра условий, например:
4 «у = 0 II г = 0». Внутренние ребра, отсутст-<5 вующие на рисунке, недостижимы.
а® Рассмотрим путь, выделенный жирным,
Ц и вершину А на нем. Может ли система пойти из этой вершины по правому ребру?
о В вершине F значение вектора состояния
^ {1, 1, 0}. Условие попадания из F в А — вы-
^ полнение предиката у = 0 II г = 0. Если пре-
§ дикат верен, ребро, входящее в А, достижи-
| мо. Достижимость ребра AS определяется
5 предикатом х = 0 II г = 1. При векторе со? стояния {1, 1, 0} значение предиката ложно, о поэтому ребро недостижимо.
5 §
* Заключение
§ Итак, в качестве результата мы получили,
^ что в клетку с критической секцией А5-В5 одновременно два потока попасть не могут,
?! т. е. критическая секция корректна.
Предложенная модель пригодна для реальных задач, в них логика и понятие корректности привязаны к конкретным значениям переменных и истории операций над разделяемыми переменными, а не к попарному их сопоставлению. Модель позволяет анализировать задачи, содержащие циклы и ветвления, формально описывает случаи взаимоблокировок, критические секции, реализованные средствами языка Си, находит неразрешенные состояния гонки. На базе этой модели может быть построено программное средство для статического анализа алгоритмов.
Список литературы
1. Кудрин М. Ю, Прокопенко А. С., Тормасов А. Г. Метод нахождения состояний гонки в потоках, работающих на разделяемой памяти // Труды МФТИ. 2009. Т. 1, № 4. С. 182-201.
2. Serebryany K. Data race test. URL: http://code. google.com/p/data-race-test.
3. Каличкин С. В. Обзор средств статической отладки. Новосибирск: Прайс-курьер, 2004. С. 22.
4. Карпов А. Тестирование параллельных программ. URL: http://www.software-testing.ru/library/testing/ functional-testing/581-parallelprogramtesting.
5. Калугин А. Верификатор программ на языке С LINT. URL: http://www.viva64.com/go.php? url=224.
6. Intel Architecture Software Developer's Manual. Volume 1.
7. Emanuelsson P., Nilsson U. A Comparative Study of Industrial Static Analysis Tools. Elsevier Science Publishers B. V. Amsterdam. The Netherlands. 2008.
8. Static Source Code Analysis Tools for C. URL: http:// www.spinroot.com/static/.
9. Использование Thread Analyzer для поиска конфликтов доступа к данным. URL: http://ru.sun.com/ developers/sunstudio/articles/tha_using_ru. html.
10. Herlihy M, Luchangco V., Moir M. Obstruction-free synchronization: Double-ended queues as an example // International Conference on Distributed Computing Systems. Providence, RI, USA 2003. P. 522-529.
11. Herlihy M, Shavit N. The Art of Multiprocessor Programming. Elsevier Science Publishers. Amsterdam. The Netherlands. 2008.