1 Introduction
2 A C11-Style Memory Model: RC11-RAR
2.1 Message Passing
2.2 Deductive Reasoning for Weak Memory
-
Possible value A possible value assertion (denoted \(x \approx _t n\)) states that thread t can read value n of global variable x, i.e. there is a write to x with value n beyond or including the viewfront of thread t. Note that there may be more than one such write, and hence there may be several possible values for a given variable. For instance, there might be one write to x with value \(v_1\) in thread t’s viewfront and two more writes to x with values \(v_2\) and \(v_3\) beyond the viewfront. Then, assertions \(x\approx _t v_1\), \(x\approx _t v_2\) and \(x\approx _t v_3\) all hold.
-
Definite value A definite value assertion (denoted \(x =_t n\)) states that thread t’s viewfront is up-to-date with the writes to x (i.e. there is a single write to x beyond or including the viewfront of thread t), and this write updates x’s value to n. Thus, t definitely knows the variable x to have value n.
-
Conditional value A conditional value assertion (denoted \([x = n](y =_{t}{m})\)) captures the message passing idiom for variable y via variable x. It guarantees that when thread t reads x to be n via an acquiring read, a release-acquire synchronisation is induced and thereby t learns the definite value of y to be m. In particular, after reading \(x = n\) via an acquiring read, the viewfront for t is updated so that the only write to y beyond or including this viewfront is a write with value m.
3 Owicki–Gries in Isabelle/HOL
WHILE
language augmented with shared-variable parallelism (||) and synchronisation (AWAIT
). Parallelism must not be nested, i.e. within \(c_1 ~||~ c_2 ~|| ~...~ ||~ c_n\), each \(c_i\) must be a sequential program. The programming language allows program constructs to be annotated with assertions in order to record proof outlines that can later be checked. The language also allows unannotated commands that may be placed within the body of AWAIT
statements. As in the original treatment [31], AWAIT
is an atomic command that executes under the precondition of the AWAIT
block.(" ... ")
. \(\alpha \) assn
and \(\alpha \) bexp
represent assertions and Boolean expressions, respectively. AnnBasic
represents a basic (atomic) state transformation (e.g. an assignment). AnnSeq
is sequential composition, AnnCond
is conditional, AnnWhile
is a loop annotated with an invariant, and AnnWait
is a synchronisation construct. The command Parallel
is a list of pairs (c, q) where c is an annotated (sequential) command and q is a post-condition. The concrete syntax for parallel composition (not shown above) is: COBEGIN
\(c_1~\{\!|q_1 |\!\} ~ || ~...~ ||~ c_n ~ \{\!|q_n |\!\}\) COEND
.oghoare
. Application of this tactic results in generation of all standard Owicki–Gries proof obligations, each of which can be discharged either automatically or via an interactive proof. We omit the full details of standard semantics and verification condition generation [30].oghoare
tactic generates 29 proof obligations, each of which is automatically discharged.4 Extending Owicki–Gries to C11-Style Memory Models
WrX
, WrR
, RdX
, ...will be instantiated in Sect. 4.2, and for the time being can be considered to be transition functions that construct a new weak memory state for a given weak memory prestate. However, a reader may wish to first read Sect. 4.2 for an example C11 memory model prior to continuing with the rest of this section.T
), the shared variable (or location) that is accessed (of type L
) and the value in that variable (of type V
).4.1 Syntactic Extension
SWAP[x, v]
command [14, 41] that acquiringly reads x
and releasingly writes v
to x
in a single atomic step. This command is used in Peterson’s algorithm (see Fig. 12) and is implemented in our model using a read–modify–write update._AnnWrX
defines a relaxed write. Its first argument is an assertion (the precondition) of the command, the second is the variable being modified, the third is the thread performing the operation, the fourth is the value being written, and the fifth is the weak memory prestate. Similarly, _AnnWrA
is a releasing write. _AnnRdX
defines a relaxed read, which loads a value of the given location (of type L
) from the given weak memory prestate into the second argument (of type idt
). An acquiring read, defined by _AnnRdA
, is similar. Finally, _AnnSwap
defines a swap operation that writes the given value (third argument) to the given location (second argument) using an update operation.translation
, which updates the program variables, including the weak memory state. For the commands above, after omitting some low-level Isabelle details, we have: These translations rely on an operational semantics defined by functions WrX
(relaxed write), WrR
(releasing write), RdX
(relaxed read), RdA
(acquiring read) and Upd
(RMW update), which we define in Sect. 4.2.
WrX
and WrA
, respectively. A read event must return a post state (which is used to update the value of the embedded weak memory state) and the value read (which is used to update the value of the local variable storing this value). In order to preserve atomicity of the read, we wrap both updates within an annotated AWAIT
statement. The translation of a SWAP
is similar.RdX
(RdA
), which one could mistakenly believe to cause two different effects on the weak memory state. However, as we shall see, these operations are implemented using Hilbert choice (SOME
), hence, although there may be multiple values that a read could return, the two applications of RdX
(RdA
) are identical for the same value for the same parameters.4.2 Operational Semantics of C11 RAR in Isabelle/HOL
WrX
, WrR
, RdX
, RdA
and Upd
from Sect. 4.1.L
represents shared variables (or locations), T
represents threads, and V
represents values. We use type TS
(which is synonymous with rational numbers) to represent timestamps. Each write can be uniquely identified by a variable-timestamp pair. The type Cstate
is a nested record with fields-
writes
, which is the set of all writes, -
covered
, which is the set of covered writes (recalling that covered writes are used to preserve atomicity of read–modify–write updates), -
mods
, which is a function mapping each write to a write record (see below), -
tview
, which is the viewfront (typeL
\(\Rightarrow \)(L
\(\times \)TS)
) of each thread, and -
mview
, which is the viewfront of each write.
val
, which is the value written and rel
, which is a Boolean that is True
if, and only if, the corresponding write is releasing.
tview
. To define their behaviours, we first define a function visible_writes
\(\sigma \) t x
4 that returns the set of writes to \(\mathtt{x}\) that thread \(\mathtt{t}\) may read from in state \(\sigma \). For a write \(\texttt {w} = \texttt {(x, q)}\), we assume a pair of functions \(\mathtt{\texttt {var}}\ \texttt {w} = \texttt {x}\) and \(\mathtt{{\texttt {tst}}}\ \texttt {w} = \texttt {q}\) that return the variable and timestamp of \(\texttt {w}\), respectively. Thus, we obtain: We use a function getVW
to select some visible write from which to read:
RdX t w
\(\sigma \) and RdA t w
\(\sigma \) that update the tview
component of \(\sigma \) for thread t
reading write w
. Function RdX t w
\(\sigma \) updates \(\texttt {tview}\ \sigma \ \mathtt{t}\) to \((\texttt {tview}\ \sigma \ \mathtt{t})[\texttt {var}\ w := w]\), where \(f [x := v]\) denotes functional override. That is, the viewfront of thread t
for variable var w
is updated to the write w
that t
reads. The viewfronts of the other threads as well as the viewfront of t
on variables different from var w
are unchanged. Thus, the function RdX
required by the translation of a relaxed read command in Sect. 4 is thus defined by: We use value
\(\sigma \) w
to obtain the value of the write w
in state \(\sigma \). The update defined by function RdA t w
\(\sigma \) for an acquiring read is conditional on whether w
is a relaxed write. If w
is relaxed, \(\texttt {tview}\ \sigma \ \mathtt{t}\) is updated to \((\texttt {tview}\ \sigma \ \mathtt{t})[\texttt {var}\ w := w]\) (i.e. behaves like a relaxed read). Otherwise, the viewfront of t
must be updated to “catch up” with the viewfront of w
. In particular, \(\texttt {tview}\ \sigma \ \mathtt{t}\) is updated to \((\texttt {tview}\ \sigma \ \mathtt{t}) \otimes (\texttt {mview}\ \sigma \ \mathtt{w})\), wherecovered
. First, following Doherty et al. [14], we must identify an existing write w
in the current state; the new write is to be inserted immediately after w
. Moreover, w
must be visible to the thread performing the write and covered by an RMW update. We define the following function: where NC
stands for “not covered”. The write operation must also determine a new timestamp, \(\mathtt{ts}\) for the new write. Given that the new write is to be inserted immediately after the write operation w
, the timestamp \(\mathtt{ts}\) must be greater than \(\mathtt{tst\ w}\) but smaller than the timestamp of other writes on \(\texttt {var}\ \mathtt{w}\) after \(\mathtt{w}\). Thus, we obtain a new timestamp using: Finding such a timestamp is always possible since timestamps are rational numbers (i.e. are dense).
write_trans t b w v
\(\sigma \) ts
that updates the state \(\sigma \) so that a new write w’ = ((var w), ts)
for thread t
is introduced with write value v
. The Boolean b
is used to distinguish relaxed and releasing writes. The write w
is the write after which the new write w’
is to be introduced. The effect of write_trans
is to update \(\texttt {writes}\ \sigma \) to \(\texttt {writes}'\), \(\texttt {mods}\ \sigma \) to \(\texttt {mods}'\) and both \(\texttt {tview}\ \sigma \ \mathtt{t}\) and \(\texttt {mview}\ \sigma \ \mathtt{w'}\) to \(\texttt {tview}'\), where:v
and the \(\texttt {rel}\) field to b
(which is \(\mathtt{True}\) iff the new write \(\texttt {w'}\) is releasing). Finally, \(\texttt {tview}'\) updates \(\texttt {tview}\) of t
for variable \(\texttt {var}\ \texttt {w}\) (the variable that both w
and w’
update) to \(\texttt {w'}\).update_trans t w v
\(\sigma \) ts
that modifies \(\sigma \) so that a releasing write w’ = ((var w), ts)
by thread t
is introduced with write value v
immediately after an existing write w
. The effect of update_trans
is to update \(\texttt {writes}\ \sigma \) to \(\texttt {writes}'\), \(\texttt {covered}\ \sigma \) to \(\texttt {covered}'\), and \(\texttt {mods}\ \sigma \) to \(\texttt {mods}'\), and both \(\texttt {tview}\ \sigma \ t\) and \(\texttt {mview}\ \sigma \ \texttt {w'}\) to \(\texttt {tview}'\), wherew’
corresponding to the update to the set of writes of \(\sigma \) and \(\texttt {covered}'\) adds the write w
that w’
reads from to the covered writes set of \(\sigma \). The new \(\texttt {mods}'\) sets the value for \(\texttt {w'}\) to v
and sets the rel
field to True
. Finally, \(\texttt {tview}'\) updates \(\texttt {tview}\) of t
in the same way as a read operation, except that the first case is taken provided the write w
being read is releasing.wfs
defined below. Function writes_on
\(\sigma \) x
returns the set of writes in \(\sigma \) to variable x
. Function lastWr
\(\sigma \) x
returns the write on x
whose timestamp is greater than the timestamp of all other writes on x
in state \(\sigma \).
wfs
\(\sigma \), the first two conjuncts ensure that all writes recorded in \(\texttt {tview}\) and \(\texttt {mview}\) are consistent with writes
\(\sigma \). The third ensures the set of writes in \(\sigma \) is finite and the fourth ensures that for each write in \(\sigma \), the write’s modification view of the variable, it writes is the write itself. The final conjunct ensures that the last write to each variable (i.e. the write with the largest timestamp) is not covered. We have shown that wfs
is stable under each of the transitions WrX
, WrR
, .... Thus, the well-formedness assumption made by the lemmas in Sect. 5 is trivially guaranteed.5 An Assertion Language for Verifying C11 Programs
5.1 Load Buffering
x
in thread 1 and y
in thread 2. Thus, the program guarantees the postcondition that \(r1 = 0 \vee r2 = 0\), i.e. the program does not terminate in the state \(r1 = 1 \vee r2 = 1\).lb_state
record for the load-buffering example). Updates to \(\sigma \) are via the underlying definition of the operations in accordance with the RC11-RAR operational semantics as described in Sect. 4.2. In our encoding, shared variables are represented as constants representing locations in the C11 state (\(\sigma \)).t
’s viewfront is consistent with the last write to x
in \(\sigma \) and that this write has value n
. Thus, if t
reads from x
, it is guaranteed to return n
. \(\sigma \). Formally,
d_obs_RdX_pres
and d_obs_WrX_diff_var_pres
give conditions for preserving definite value assertions for relaxed read and write transitions, respectively, for an arbitrary variable y
and thread t’
. Note d_obs_WrX_diff_var_pres
requires that the variable y
that is written to is different from the variable x
that appears in the definite value assertion. Both lemmas are proved sound with respect to the operational semantics in Sect. 4.2. Of course, d_obs_RdX_pres
also holds for an acquiring read transition and d_obs_WrX_diff_var_pres
for a releasing write transition5.d_obs_RdX_pres
. Local correctness of the assertions on lines 18 and 24 is trivial follows by the definite value assertion. Interference freedom of the assertions in lines 14, 16, 20 and 22 is also established using the two lemmas.5.2 Message-Passing
[x
\(\approx _{\mathtt{t}}\) n]
\(\sigma \)) states that thread t
may read value n
if it reads from variable x
. The formal definition in Isabelle is as follows: The next assertion we introduce is the conditional observation assertion (denoted [x = n]
y
\(=_{\mathtt{t}}\) m
\(\sigma \)) which states that if thread t
reads a value n
using an acquiring read for x
, it synchronises with the corresponding write and obtains the definite value m
for y
. Note that this requires that any write to x
with value n
that t
can read is a releasing write. The formal definition in Isabelle is as follows:
c_obs_intro
. In particular, the assertion at line 13 (i.e. the precondition of line 14) satisfies the critical premises of c_obs_intro
. We use the conditional observation assertion (line 17 of thread 2) in combination with rule c_obs_transfer
to establish a definite observation on a new variable in thread 2. We note that the variable read by the transition in rule c_obs_transfer
is x
, whereas the definite value assertion in the consequent is on variable y
. Full details of this proof may be found in [9]; in this paper, we focus on automation, which we discuss in Sect. 7.5.3 Read–Read Coherence
-
The first thread (i.e. thread 1) updates x to 1, then signals that this has been done using a releasing write to y.
-
The second thread (i.e. thread 2) reads y using an acquiring read, then updates x to 2.
-
The third thread (i.e. thread 3) performs two successive reads of x.
[m
\(\prec _\texttt {x}\) n]
\(\sigma \)), which states that there exists a write to variable x
with value n
ordered after (i.e. with a greater timestamp) a write to x
with value m
. Similarly, a definite value order assertion (denoted [m
\(\prec \!\!\!\prec _\texttt {x}\) n]
\(\sigma \)) states that all writes to x
with value n
are ordered after all writes to x
with value m
. These are formally defined in Isabelle as follows:
n
to variable x
(where i
is the initial value of x
) and
means that there has been at most one write with value n
to x
. These two assertions are defined in terms of ordering assertions introduced earlier as follows. The predicate init
\(\sigma \) x n
holds iff the initial value of x
in \(\sigma \) is n
. The last new assertion used in this proof outline is encountered value (denoted as [
\(\texttt {x} {\mathop {=}\limits ^{\textit{enc}}}_{\texttt {t}} \texttt {n}\)]
) means that thread t
has had the opportunity to observe a write with value n
of x
. This assertion is formally defined in Isabelle as follows6:
r1
is set to 1), then it must have observed the write of x
at line 12. Thus, the write to x
with value 2 at line 23 must have happened after. Therefore, it must be impossible for the third thread to read value 2 for x
at line 27, then subsequently read 1 for x
at line 29. This reasoning is captured by the postcondition of the program.5.4 Two-Way Message-Passing
r
and w
. Thread 2 reads the value of w
and writes it to r
twice. Thread 1 writes 1 to w
and then waits until it sees 1 for r
to terminate. The goal here is to prove that once the program is terminated, the only visible value for r
is 1. We stated this property as follows:
r
and it is impossible for thread 1 to see any value for r
which is not equal to 1.6 Case Studies
6.1 Peterson’s Algorithm for C11
SWAP
command. The SWAP
is implemented using an C11 RMW operation that has both the release and acquire annotations. When the SWAP
is executed, as part of the same transition, the auxiliary variable \(\textit{after}_1\) is also set, indicating that thread 1 is ready to enter the busy wait loop beginning at line 18, and then to enter the critical section.cvd[x, n]
\(\sigma \)). The assertion cvd[x, n]
\(\sigma \) means that every write to x
in state \(\sigma \) except the last is covered and the value written by that last write is n
. This assertion is formally defined in Isabelle as: Similar to the previous examples, in order to prove the Peterson’s algorithm we will need to introduce new proof rules to deal with assertions involving cvd
. Here, we present couple of the most interesting proof rules related to the covered assertion:
x
are covered with value u
in the pre-state, and we perform an update operation on the same variable which writes value v
, then all the writes to x
in the post-state are covered with value v
. Lemma \(\mathtt{cvd\_c\_obs\_transfer}\) states that if in the pre-state, we have a conditional observation and also we know that all writes to x
are covered, then any update operation on x
by thread t
transfers the definite observation on y
to thread t
. The final lemma states that read operations preserve covered assertions. All the above lemmas have been proved in Isabelle.6.2 Read–Copy–Update (RCU)
-
use a set of shared reader variables (one for each reader),
-
each reader thread is a copy of the reader in Fig. 13, and publishes confirmation that it has seen the update to w using its own reader variable, and
-
the writer repeats the do-until loop for each additional reader, waiting for the corresponding reader variable to be set to 1.
7 Verification and Automation
oghoare
tactic to reduce an Owicki–Gries proof outline into a set of basic Hoare logic proof obligations over weak memory pre-post state assertions. This tactic is exactly as developed by Nipkow and Nieto [30], and is used without change.apply
command:
oghoare
tactic to generate OG proof obligations (in the form of Isabelle subgoals) and then applies a series of simplification to each of the generated proof obligations.Algorithm | Algorithm and proof outline size (LOC) | Number of threads | POs generated by oghoare | Proof automation (%) | Approx. time (s) |
---|---|---|---|---|---|
Load buffering | 15 | 2 | 24 | 100 | 2 |
Message passing | 19 | 2 | 41 | 100 | 2 |
RRC 2 | 15 | 2 | 24 | 96 | 2 |
RRC 3 | 21 | 3 | 59 | 90 | 5 |
RRC 4 | 23 | 4 | 64 | 98 | 3 |
Two-way MP | 57 | 2 | 72 | 20 | 11 |
Peterson’s | 80 | 2 | 258 | 65 | 89 |
RCU | 64 | 2 | 182 | 65 | 70 |
sledgehammer
tool [7].oghoare
tactic generates 258 subgoals for Peterson’s and 182 for RCU, but over half of these are discharged by the above apply command. Although automatic, repeated applications of sledgehammer
to discharge so many proof obligations is rather tedious. However, one can quickly discover common patterns in the proof steps allowing these proof obligations to be discharged via a few simple applications of apply
-style proofs.sledgehammer
tool fail to find a proof automatically, we either lack an essential rule in the proof rule set or the proof outline is not provable. If we manage to identify and prove a proof rule that can assist the automatic provers to discharge the proof obligation, then we will extend the proof rule set. However, if a proof rule is not found, we should consider the possibility of non-provable proof outline where either the precondition needs weakening or the postcondition needs strengthening.sledgehammer
in finding a proof for a proof obligation automatically. These failures have also led the development of proof outlines, where in most cases the generated proof obligations hinted that what is missing in the pre-condition.