Дедуктивный метод анализа логических гонок с использованием сепарационной логики

УДК 004.451.2

А.С. Прокопенко, А.Г. Тормасов

Московский физико-технический институт

Дедуктивный метод анализа логических гонок с использованием сепарационной логики

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

Ключевые слова: параллельное программирование, верификация программ, логика Хо-ара, сепарационная логика, логическая гонка.

I. Введение

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

Проверка соответствия выполнения параллельных программ заданной спецификации осложнена тем, что порядок, в котором будут выполнены инструкции разных потоков, заранее непредсказуем, и необходимо рассмотреть всевозможные варианты выполнения программ. Одна из главных сложностей, связанных с проверкой правильности программ, — это «комбинаторный взрыв» в пространстве состояний системы — значительный рост объема проверок даже при небольшом усложнении комплекса программ. В том случае, когда спецификация параллельных программ может быть разбита на отдельные свойства, описывающие поведение небольших фрагментов системы, возможно применить стратегию проверки каждого локального свойства, используя только тот фрагмент системы, который описывается лишь этим свойством. Допустим, что имеются n потоков Ti, T2,...Tn, исполняющихся одновременно. Поскольку поведение потока Ti зависит от поведения других потоков, именуемых средой, пользователь формулирует ряд допущений (Rely), которые должны выполняться средой, для того чтобы гарантировать корректность потока Ti. Поскольку поведение среды также зависит от поведения потока Ti, пользователь формулирует ряд допущений (Guarantee), которые должны выполняться потоком, для того чтобы гарантировать корректность среды. Построив долж-

ным образом комбинацию допущений потоков, можно установить правильность всей системы Ti ||T21|. ..\\Tn. Этот подход, называемый композиционным (Rely/Guarantee,Assume/Guarantee), активно используется прежде всего в методах проверки на основе моделей (model checking) и в дедуктивной верификации (theorem proving).

В дедуктивном методе Rely/Guarantee —R/G[1] (Jones,1983) анализ исполнения программы выполняется посредством применения правил вывода к программным инструкциям. Существенным недостатком являются необходимость спецификации инвариантов всех общих данных. Применение сепарационной логики (Separation Logic [2]) для модернизации метода R/G позволит ликвидировать эту проблему, но в самой сепарационной логике доступ к совместно используемым данным обладает высокой степенью абстрактности, что при верификации часто влечет ложные заключения о характере выполнения параллельных про-грамм[3]. На идее синтеза дедуктивного метода R/G и сепарационной логики создано несколько методов: SAGR [3] (Feng, 2007), RGSep [4] (Vafeiadis, 2007), LocalRG [5] (Feng, 2009). Однако два первых метода не позволяют явно рассматривать программы, в которых некоторые потоки имеют общие области памяти, скрытые от других потоков, что отрицательно отражается на результатах анализа параллельных программ, к тому же SARG в качестве команд рассматриваются ассемблерные инструкции на архитектуре RISC, что в целом затрудняет процесс верификации. Главным недостатком LocalRG является требование точного инварианта [5]. Также LocalRG, SARG не подходят для верификации свойств живучести (liveness properties). Неформально говоря, живучесть — это те свойства программ, которые утверждают, что нечто изначально задуманное произойдет при любом развитии внешних событий и любом ходе работы собственно программы.

II. Цель работы

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

Для этого необходимо решить следующие задачи:

• внести изменения в модель состояний композиционного метода RGSep;

• ввести новое правило сокрытия ресурсов;

• внести необходимые правки в другие правила RGSep логики;

• доказать непротиворечивость предложенной логики.

III. Модель памяти, сепарационная логика

В оригинальном методе Rely/Guarantee модель памяти наследована от модели памяти, традиционно используемой в логике Хоара:

Values = {..., — 2, — 1, 0, 1, ...};

Vars = {x, y, ...};

Stores = Vars ^ Values.

Но такое представление не подходит для описания динамических структур. Этот пробел восполняет сепарационная логика. Пока, в силу определенных причин, еще не выработан единый стандарт этой логики, мы в своей работе руководствуется следующим представлением памяти сепара-ционной логики:

Values = {..., — 2, — 1, 0, 1,...};

Vars = {x, y, ..., &x, &y, ...};

Locations = {1, 2, ...};

Stores = Vars ^ Values;

Heaps = Locations ^ finValues;

£ = Stores x Heaps.

А сами программные состояния характеризуются через утверждения (формулы состояния памяти): пусть £ — пространство программных состояний, B — булево множество {0, 1}, тогда утверждением p над пространством состояний называем предикат вида p = {а\а : £ ^ B}.

Пусть p и q являются утверждениями в сепа-рационной логике:

p, q ::= false\emp\e = e'\e ^ e'\p ^ q\p*q\p — <g)q|...

Здесь emp введено для обозначения пустой кучи

* является ключевым элементом сепарационной логики. Куча h удовлетворяет p * q, если ее можно разбить на две части, одна удовлетворяет утверждению p, другая — q: (t,h)\ = (p * q) &3 hi, h/2, hi l±l h-2 = h Л (hi | = p) Л (h21 = q).

Если для всех hi, h[, h2, h2 С h: (hi Ы h2 = hi Ы h'2) Л (t,hi\ = p) Л (t,h'i \ = p) следует hi = hi, то утверждение p = {a\a : £ ^ B} называется однозначным утверждением precise (p).

В нашей модели мы различаем локальные и общие ячейки памяти (среди общих ячеек памяти, также могут быть доступные всем или только части потокам), поэтому состояние потока — это набор двоек (l,s) G £2. Тогда пусть запись p — утверждение относительно общих ячеек памяти, а p — локальных. Синтаксис:

p, q ::= P\Q\p\q\p * q\p A q\p V q\p - q.

Отношение | = выполнимости утверждения в состоянии а = (s,l) определяется по индукции:

(l,s)\ = P & l\ = sepbP; (l,s)\ = P & l\ = sepbP;

(^s)l = R (l = 0) А И = SepbP)',

(/,s)| = P * Q -ФФ- (/ = 0) Л Э si, so',

(s = si i±l so) A (si| = sepbE’) A (s21 = sepbQ)', a\ = pi * p2 &

3 ai,a2(a = alLa2)Л(al \ = sepLpi)A(a2 \ = SepLp2);

a\ = pi Vp2 & (а\ = SepLpi) V (а\ = SepLp2);

a\ = pi Л p2 & (а\ = SepLpi) Л (а\ = SepLp2); a\ = p — q &

3 ai,a2(a2 = aiLa) Л (ai| = SepLp) Л (а2\ = SepLq).

IV. Представление команд программы

Параллельная программа представляет собой последовательность из инициализирующих команд и набора одновременно исполняющихся потоков Co; (Ci||...||Cn). Каждый поток состоит из операторов.

Определим синтаксис языка программирования следующим образом:

• команды:

C ::= skip\x := E\x := [E]\[E] := E\C;

C\ifB th en{C }else{C}\ \whileBdo{C}\x :=

:= new()|delete(E)|CAS([E],x,x);

• целочисленные выражения: E ::= x\n\E iop E;

• логические выражения: B ::= b\E bop E.

Вспомогательные определения: n G Z — множество целых чисел; b G B = {true, false} — множество логических значений; iop G lop = {+, — ,,...} — постоянное конечное множество целочисленных бинарных операций; bop G Bop = {= ,<,>,...} — постоянное конечное множество логических бинарных операций. Запись [x] означает содержание ячейки по адресу x. Оператор skip (пустой оператор) не меняет состояния программы.

V. Действия

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

Rely- и Guarantee-условия определяют допустимость изменений общих состояний. В нашей модели введем понятие действий (actions) p ~> q, которые определяют переходы между общими состояниями, специфицированными утверждениями p и q:

(si * so, s2 * so)| = p ^> q &

& s11 = p Л s11 = q;(si, s2)\ = [p] &

& si = s2 Л si| = p;

(s, s')\ = a * a' &

& s = si g s2 Л s' =

= si g s'2 Л (si, si| = ai) Л (s2, s'2\ = a');

(si, s2)\ = a ^ a',

если из (si,s2)| = a следует (si,s2)| = a'.

Первая формула специально записана в расцепленном на две части виде для уменьшения работы при верификации. Аналогично можно записать и

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

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

Будем говорить, что утверждение р устойчиво под действием а Б1аЬ(р, а) тогда и только тогда, когда V в,в', таких, что в| = р А (в,в')| = а следует в'1 = р.

Доказана лемма 1. Б1аЬ

I = (г — р) * д ^ р.

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

Доказана лемма 2. Б1аЬ(р,(К\ и К2)*) Б1аЬ(р,В,1) А 31аЬ(р,В,‘2).

Вторая лемма о том, что утверждение р устойчиво под множеством действии К тогда, когда устойчиво под действием каждого.

Доказана лемма 3. Б1аЬ(р1,а) А Б1аЬ(р2,а) ^ 81аЬ(р\Ар2,а); Б1аЬ(р1,а\)У Б1аЬ(р2,а2) ^ 81аЬ(р\У р2,а\ Аа2); Б1аЬ(р,а1) АБЬаЬ^а) ^ Б1аЬ(р,а\ Аа2).

Казалось бы, сепарационная конъюнкция над действиями а * а' позволит скомбинировать различные переходы в одну запись, однако это не так.

Пример. Пусть Ь\ ^ 1, а,1 = {Ь2 ^ 2, Ь2 ^ 3} и Ь2 ^ 4, а2 = {Ь\ ^ 5, Ь\ ^ 6}, причем

= Ь2, то есть имеем Б1аЬ(р1,а), Б1аЬ(р2,а) и Б1аЬ(р1 *р2,а\ * а2) — ложь.

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

Будем говорить, что действие а защищено точным инвариантом I > а тогда и только тогда, когда инвариант имеет место в начальном и конечном переходах, удовлетворяющих а, и является точным инвариантом ргеавв(1): а ^ (I ~> I) А ргеаве(1).

Доказана лемма 4.

(Б1аЬ(р1,а\) А Б1аЬ(р2,а2) А р\ ^ I) А (I > а) ^

^ Б1аЬ(р1 * р2, а,1 * а2).

VI. Правила вывода

В оригинальном методе К/О спецификация каждого потока задается четверкой (рге, К, О, ровЬ). В нашем методе формулу частичной корректности (частичной в смысле того,

что завершение работы программного кода не является обязательным условием) запишем как R; G; I\ — {pre}C{post}, это означает, что если предусловия удовлетворяют утверждению pre, изменения общих состояний средой выполняются в соответствии с R, выполнение кода потока удовлетворяет G при защите точным инвариантом I, и если подпрограмма завершает работу, то постусловия удовлетворяют утверждению post.

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

По сравнению с логикой RGSep введено понятие сепарационной конъюнкции для спецификации Ri *R2, Gi *G2, инварианта Ii * I2; введено два новых правила для скрытия части общих данных и «разбивки» локальных:

R;G;I\-{P*Y}C{Q*¥} St,ab(M_,R) I>{R',G'} I1*true

R*R',G*G\I*I'\-{P*g*Y}C{Q*K*Y!}

R*R/;G*G/;I*r\-{p}C{q} I>{R,G} R,G,II - {p}C{q} '

В рамках нашего метода «традиционные» правила дедуктивных методов на основе R/G принимают следующий вид:

R,G,II — {p)'e}C{post} Stab

R,G,I\ — {pre * p}C{post * p}

- {pre'}C{post'} RCR1 G1 С G pre => pre1 post1 => post R,G,I\ - {pre}C{post}

R; G; /1 — {p)'ei}C{post} R; G; /1 — {pre2}G{posf} R; G; I\ — {prei U pre2}C{post} R;G;I\ — {p)'e}C{posti} R;G;I\ — {prejClpos^} R;G;I\ — {p/’e}G{posfi flpos^} Stab

R; G; I \ — {pre} skip {post} ’

R,G,I I — {p)'e}C{post} R,G,II — |pre}G * {post} ’

Д; G; I\ - {pjGilr} Д; G; I\ - {r}C2{g} R;G;I\-{p}C1;C2{q} '

К тем командам, которые не имеют доступа к общим данным, нужно использовать адаптированные правила из сепарационной логики:

I - sePb{P}c{Q} R,G,I\-{Q}c{QY

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

R U Go\ G\; 11 - {P\ * ZjCtiQt * I > {ВД BUG1;G2;/| - {P2 *Z}Co{Qo * 7^} TU Tt UTo => I R; G\ U G2; /| - {Pi * Po * T}Gi \\Со{Рг * P2 * (i?j A i?2)}'

В качестве примера здесь приведено правило для исполнения команд во взаимоисключающем режиме для простого случая (обозначаем как atomic):

p~>qcg puq^j />{ад іціщд) Pi~>QiCG

Л; G; /| - {Pi * P}eioj);ic{G}{gi * Q}

VII. Доказательство непротиворечивости

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

Введем определение истинности формулы частичной корректности: К; О; 11 = {р}С{д} ^ 1,в| = р, Оиаг(О,С,(1,в),К), (С,(1,в)) *(вЫр,(1',в')),

1',в'1 = д.

Доказана лемма 5. Если программный код С является примитивом, то возмо-один из трех сценариев:

жен

R

R

трех сценариев:

*

* fault & 3a'' .(C,a) ~R^e (C,a'') ~R^p fault;

(C,a) *(skip,a') & 3 a''a"'.(C,a) —^е

(C,a'') —~^p (skip,a''') e (skip,a').

Доказана лемма 6 об условии G при параллельном исполнении. Если Guar(Gi,Ci,a,R U G2), Guar(G2,C2,a,R U Gi) и ai о a2 = a, то

Guar(Gi U G2,Ci\\C2,a,R); если (С^С^) R

RJG2

*(С[11С2,а/), то За/,а'2.(С1,а1) *(С1,а[),

(С2,а2) *(С2,а2),а/ о а'2 = а'.

Доказана лемма 7 об условии О при последовательном исполнении. Если Оиаг(О,С/,а,К), V а' : (С/,а) *(вЫр,а'), Оиаг(О,С2,а',К), то

Оиаг(О,С1; С2,а,К).

Доказана лемма 8. Если

*(вЫр,а''), тогда З а' : (С/,а) *(вЫр,а') и

(С2,а') *(вк1р,ап).

Для интерпретации выводимости как доказательства в смысле частичной корректности необходимо, чтобы система вывода обладала свойством непротиворечивости, то есть чтобы при выполнении общезначимых формул получались общезначимые, что отражает доказанная теорема о том, что предложенная система вывода непротиворечива для свойства частичной корректности: если К; О; I —{р}С{р}, то К; О; 11 = {р}С{р}. Программа считается прошедшей проверку, если для каждого потока с помощью применения аксиом и правил показан вывод формулы корректности.

VIII. Практическое применение

С помощью метода верифицированы на отсутствие гонок несколько типичных программ добавления/удаления элемента из очереди, стека, алгоритм «булочной». Причем в части из этих программ механизмы синхронизации реализованы с помощью структур свободных от блокировок (lock-free).

IX. Заключение

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

Дальнейшая работа планируется в направлении исследования автоматизации процесса анализа на базе инструментов типа SmallfootRG, Isabelle/HOL.

Литература

1. Jones C. Specification and design of (parallel) programs // Proceedings of IFIP ’83. — North-Holland, 1983. — Р. 321-332.

2. Reynolds C. Separation logic: A logic for shared mutable data structures // Proc. 17th Annual IEEE Symposium on Logic in Computer Science (LICS’02). — IEEE Computer Society. — July 2002. — P. 55-74.

3. Feng X., Rodrigo F, Zhong S. On the relationship between concurrent separation logic and assume-guarantee reasoning // Proc. 16th European Symp. on Prog. (ES0P’07). — Vol. 4421 of Lecture Notes in Computer Science. — Springer, March 2007. — P. 173-188.

4. Vafeiadis V., Parkinson M. A marriage of rely / guarantee and separation logic // Proc. 18th Int’l Conf. on Concurrency Theory (C0NCUR’07). — Vol. 4703 of Lecture Notes in Computer Science. — September 2007. — P. 256-271.

5. Feng X. Local rely-guarantee reasoning // ACM Symposium on Principles of Programming Languages. — ACM, 2009. — P. 315-327.

Поступила в редакцию 27.09.2010.