Jones CB, Velykis A, Yatapanage N.
General Lessons from a Rely/Guarantee Development.
In: 3rd International Symposium on Dependable Software Engineering:
Theories, Tools, and Applications (SETTA). 2017, Changsha, China: Springer
Verlag
Copyright:
The final publication is available at Springer via https://doi.org/10.1007/978-3-319-69483-2_1
DOI link to article:
https://doi.org/10.1007/978-3-319-69483-2_1
Date deposited:
22/11/2017
This work is licensed under a Creative Commons Attribution-NonCommercial 3.0 Unported License
Newcastle University ePrints - eprint.ncl.ac.uk
General Lessons from a Rely/Guarantee Development
Cliff B. Jones1 , Andrius Velykis1 , Nisansala Yatapanage1,2
1
2
School of Computing Science, Newcastle University, United Kingdom
School of Computer Science and Informatics, De Montfort University, United Kingdom
Abstract. Decomposing the design (or documentation) of large systems is a
practical necessity; this prompts the need for a notion of compositional development methods; finding such methods for concurrent software is technically
challenging because of the interference that characterises concurrency. This
paper outlines the development of a difficult example in order to draw out
lessons about such development methods. Although the “rely/guarantee” approach is employed in the example, the intuitions are more general.
1
Introduction
The aim of this paper is to contribute to the discussion about compositional development for concurrent programs. Much of the paper is taken up with the development,
from its specification, of a concurrent garbage collector but the important messages
are by no means confined to the example and are identified as lessons.
1.1 Compositional methods
To clarify the notion of “compositional” development of concurrent programs, it is
worth beginning with some observations about the specification and design of sequential programs. A developer faced with a specification for S might make the design decision to decompose the task using two components that are to be executed
sequentially (S1; S2); that top-level step can be justified by discharging a proof obligation involving only the specifications of S and S1/S2. Moreover, the developer of
either of the sub-components need only be concerned with its specification — not
that of its sibling nor that of its parent S. This not only facilitates separate development, it also increases the chance that any subsequent modifications are isolated
within the boundary of one specified component.
As far as is possible, the advantages of compositional development should be
retained for concurrent programs.
Lesson 1 The notion of “compositionality” is best understood by thinking about a
development process: specifications of separate components ought genuinely insulate
them from one another (and from their context). The ideal is, faced with a specified
task (module), propose a decomposition (combinator) and specify any sub-tasks; then
prove the decomposition correct wrt (only) the specifications. The same process is then
repeated on the sub-tasks.
Because of the interference inherent in concurrency, this is not easy to achieve
and, clearly, (pre/)post conditions will not suffice. However, numerous examples exist to indicate that rely/guarantee conditions (see Section 1.2) facilitate the required
separation where a designer chooses a decomposition of S into shared-variable subcomponents that are to be executed in parallel (S1 || S2).
1.2 Rely/Guarantee thinking
The origin of the rely/guarantee (R/G) work is in [Jon81]. More than 20 theses have
developed the original idea including [Stø90,Xu92] that look at progress arguments,
[Din00] that moves in the direction of a refinement calculus form of R/G, [Pre01]
that provides an Isabelle soundness proof of a slightly restricted form of R/G rules,
[Col08] that revisits soundness of general R/G rules, [Pie09] that addresses usability and [Vaf07,FFS07] manage to combine R/G thinking with Separation Logic. Furthermore, a number of separation logic (see below) papers also employ R/G reasoning (e.g. [BA10,BA13]) and [DFPV09,DYDG+ 10] from separation logic researchers
build on R/G. Any reader who is unfamiliar with the R/G approach can find a brief
introduction in [Jon96]. (A fuller set of references is contained in [HJC14,JHC15].)
The original way of writing R/G specifications displayed the predicates of a
specification delimited by keywords; some of the subsequent papers (notably those
concerned with showing the soundness of the system) present specifications as 5tuples. The reformulation in [HJC14,JHC15] employs a refinement calculus format [Mor90,BvW98] in which it is much more natural to investigate algebraic properties of specifications. Since some of the predicates for the garbage collection example are rather long, the keyword style is adopted in this paper but algebraic properties such as distribution are used as required.
The literature contains many and diverse examples of R/G developments including:
– Susan Owicki’s [Owi75] problem of finding the minimum index to an array at
which an element can be found satisfying a predicate is tackled using R/G thinking in [HJC14]
– a staple of R/G presentations is a concurrent version of the Sieve of Eratosthenes
introduced in [Hoa72] — see for example [JHC15]
– parallel “cleanup” operations for the Fisher/Galler Algorithm for the union/find
problem are developed in [CJ00]
– a development of Simpson’s 4-slot algorithm is given in [JP11] — an even nicer
specification using “possible values” (see Section 1.3) is contained in [JH16a]
The first two represent examples in which the R/G conditions are symmetric in
the sense that the concurrent sub-processes have the same specifications; the last two
items and the concurrent garbage collector presented below are more interesting
because the concurrent processes need different specifications.
Lesson 2 By using relations to express interference, R/G conditions offer a plausible
balance of expressiveness versus tractability — see Sections 3.2 and 4.
1.3 Challenges
The extent to which compositionality depends on the expressivity of the specification notation is an issue and the “possible values” notation used below provides
an interesting discussion point. Much more telling is the contrast with methods
which need the code of sibling processes to reason about interference. For example [Owi75,OG76] not only postpones a final (Einmischungsfrie) check until the code
of all concurrent processes is to hand — this expensive test has to be repeated when
changes are made to any sub-component.
It is useful to distinguish progressively more challenging cases of interference
and the impact that the difficulty has on reasoning about correctness:
1. The term “parallel” is often used for threads that share no variables: threads are
in a sense entirely independent and only interact in the sense that that they overlap in time. Hoare [Hoa72] observes that, in this simple case, the conjunction
of the post conditions of the individual threads is an acceptable post condition
for their combination.
2. Over-simplifying, this is a basis for concurrent separation logic. CSL [O’H07]
and the many related logics are, however, aimed at –and capable of– reasoning
about intricate heap based-programs. See also [Par10].
3. It is argued in [JY15] that careful use of abstraction can serve the purpose of
reasoning about some forms of separation.
4. The interference in Owicki’s example that is referred to in the preceding section
is non-trivial because one thread affects a variable used to control repetition
in the other thread. It would be possible to reason about the development of
this example using “auxiliary” (aka “ghost”) variables. The approach in [Owi75]
actually goes further in that the code of the combined system is employed in the
final Einmischungsfrei check. Using the R/G approach in [HJC14], however, the
interference is adequately characterised by relation.
5. There are other examples in which relations alone do not appear to be enough.
This is true of even the early stages of development of the concurrent garbage
collector below. A notation for “possible values” [JP11,HBDJ13,JH16b] obviates
the need for auxiliary variables in some cases see Section 2.1
6. The question of whether some examples require ghost variables is open and the
discussion is resumed in Section 4. That their use is tempting in order to simplify
reasoning about concurrent processes is attested by the number of proofs that
employ them.
Lesson 3 The use of “ghost” (aka “auxiliary”) variables presents a subtle danger to
compositional development (cf. Lesson 1). The case against is, however, clear: in the
extreme, ghost variables can be used to record complete detail about the environment
of a process. Few researchers would succumb to such extreme temptation but minimising
the use of ghost variables ought be an objective.
It might surprise readers who have heard the current authors inveigh against
ghost variables that the development in this conference paper does in fact use such
a variable. The acknowledgements report a relevant discussion on this point and a
journal paper is planned to explore other options.
1.4 Plan of the paper
The bulk of this paper (Sections 2–5) offers a development of the concurrent garbage
collector described in [BA84,vdS87]. At several points, “lessons” are identified. It is
these lessons that are actually the main point of the paper and the example is chosen
to give credence to these intuitions.
The development uses R/G ideas (often referred to as “R/G thinking”) but the
lessons have far wider applicability.
It is also worth mentioning that it is intended to write an extended journal version of this paper that will contain a full development from an abstract specification
hopefully with proofs checked on Isabelle. That paper will also review various modelling decisions made in the development. Many of twhich were revised (in some
cases more than once) e.g. the decision how to model Heap was revised several
times!3
2
Preliminary development
This section builds up to a specification of concurrent garbage collection that is then
used as the basis for development in Sections 3–5. The main focus is on the Collector
but, since this runs concurrently with some form of Mutator, some assumptions have
to be recorded about the latter.
2.1 Abstract spec
It is useful to pin down the basic idea of inaccessible addresses (aka “garbage”)
before worrying about details of heap storage (see Section 2.2) and marking (Section 3).
Lesson 4 It is widely accepted that the use of abstract datatypes can clarify key concepts
before discussion turns to implementation details. Implementations are then viewed as
“reifications” that achieve the same effect as the abstraction. Formal proof obligations
are given, for example, in [Jon90].
Lesson 4 is common place for sequential programs but it actually has even greater
force for concurrent program development (where it is perhaps underemployed by
many researchers). For example, it is argued in [JY15] that careful use of abstraction
can serve the purpose of reasoning about separation. Furthermore, in R/G examples
such as [JP11], such abstractions also make it possible to address interference and
separation at early stages of design.
The set of addresses (Addr) is assumed to be some arbitrary but finite set; it is not
to be equated with natural numbers since that would suggest that addresses could
have arithmetic operators applied to them.
3
One plausible alternative is:
m
Heap = (Addr × Pos) −→ Addr
The abstract states4 contain two sets of addresses: those that are in use (busy)
and those that have been collected into a free set.
Σ0 :: busy : Addr-set
free : Addr-set
where
inv-Σ0 (mk-Σ0 (busy, free))
△
busy ∩ free = { }
It is, of course, an essential property that the sets busy/free are always disjoint. (VDM
types are restricted by datatype invariants and the set Σ0 only contains values that
satisfy the invariant.) There can however be elements of Addr that are in neither
set — such addresses are to be considered as “garbage” and the task of a garbage
collector is to add such addresses to free.
Effectively, the GC process is an infinite loop repeatedly executing the Collector
operation whose specification is:
Collector
ext wr free
rd busy
pre true
rely (busy′ − busy) ⊆ free ∧ free′ ⊆ free
guar free ⊆ free′
S˜
post (Addr − busy) ⊆ free
The predicate guar-Collector reassures the designer of Mutator that a chosen free
cell will not disappear. The read/write “frames” in a VDM specification provide a
shorthand for access and interference: thus Collector actually has an implied guarantee condition that it cannot change busy.
The rely condition warns the developer of Collector that the Mutator can consume
free addresses. Given this fact, recording a post condition for Collector is not quite
trivial. In a sequential setting, it would be correct to write:
free′ = (Addr − busy)
but the concurrent Mutator might be removing addresses from the free set so the
best that the collector can promise is to place all addresses that are originally garbage
into the free set at some point in time. Here is the first use of the “possible values”
notation in this paper. In a sequential formulation, post-Collector would set the lower
bound for garbage collection by requiring that any addresses not reachable (in the
initial hp) from roots would be in the final free set. To cope with the fact that a
concurrent Mutator can acquire addresses from free, the correct statement is that all
unreachable addresses should appear in some value of free. The notation discussed
in [JP11,HBDJ13,JH16b] for the set of possible values that can be observed by a
˜
component is free.
4
The use of VDM notation should present the reader with no difficulty: it has been widely
used for decades and is the subject of an ISO standard; one useful reference is [Jon90].
Lesson 5 The “possible values” notation is a useful addition to –at least– R/G.
2.2 The heap
This section introduces a model of the heap. The set of addresses that are busy is
defined to be those that are reachable from a set of roots by tracing all of the pointers
in a heap.
Σ1 :: roots : Addr-set
hp
: Heap
free : Addr-set
where
inv-Σ1 (mk-Σ1 (roots, hp, free)) △
dom hp = Addr ∧
free ∩ reach(roots, hp) = { } ∧
∀a ∈ free · hp(a) = {[ ]}
upper bound for GC
m
Heap = Addr −→ Node
∗
Node = Addr
To smooth the use of this model of Heap, hp(a, i) is written for hp(a)(i) and (a, i) ∈
dom hp has the obvious meaning. When addresses are deleted from nodes, their
position is set to the nil value.
The second conjunct of the invariant defines the upper bound of garbage collection; the final conjunct requires that free addresses map to empty nodes. The roots
component of Σ1 is taken to be constant.
The child-rel function extracts the relation over addresses from the heap (i.e.
ignoring pointer positions); it drops any nil values.
child-rel : Heap → (Addr × Addr)-set
child-rel(hp)
△
{(a, b) | a ∈ dom hp ∧ b ∈ (elems hp(a)) ∩ Addr}
The reach function computes the relational image (with respect to its first argument) of the transitive closure of the heap:
reach : Addr-set × Heap → Addr-set
reach(s, hp)
△
rng (s ⊳ child-rel(hp)⋆ )
A useful lemma states that, starting from some set s, if there is an element a
reachable from s that is not in s, then there must exist a Node which contains an
address not in s (but notice that hp(b, j) might not be a).
A useful lemma is:
∃a · a ∈ reach(s, hp) ∧ a ∈
/ s ⇒ ∃(b, j) ∈ dom hp · b ∈ s ∧ hp(b, j) ∈
/s
3
Marking
The intuition behind the garbage collection (GC) algorithm in [BA84] is to mark all
addresses reachable (over the relation defined by the Heap) from roots, then sweep
any unmarked addresses into free.
The state underlying the chosen garbage collector has an additional component
to record the addresses that have been marked (the third conjunct of the invariant
ensures that all addresses in (roots ∪ free) are always marked).
Σ2 :: roots
hp
free
marked
:
:
:
:
Addr-set
Heap
Addr-set
Addr-set
inv-Σ2 (mk-Σ2 (roots, hp, free, marked))
dom hp = Addr ∧
free ∩ reach(roots, hp) = { } ∧
(roots ∪ free) ⊆ marked ∧
∀a ∈ free · hp(a) = {[ ]}
△
upper bound for GC
3.1 Sequential algorithm
Garbage collection runs concurrently with a Mutator which can acquire free addresses and give rise to garbage that is no longer accessible from roots. A fully concurrent garbage collector is covered in Section 4. This section introduces (in Fig. 1)
code that can be viewed as sequential in the sense that the Mutator would have to
pause; interestingly this same code satisfies specifications for two more challenging
concurrent situations (see Sections 3.2 and 4).
As observed above, the full garbage collector repeatedly iterates the code called
here Collector. This can be split into three phases. Providing the invariant is respected, Mark/Sweep do not depend on how many addresses are marked initially
(Unmark is there to ensure that garbage is collected in at most two passes) but,
thinking of the Collector being run intermittently, it is reasonable to start by removing any surplus marks.
Collector △ (Unmark; Mark; Sweep)
The main interest is in the marking phase. As shown in Fig. 1, the outer loop
propagates a wave of marking over the hp relation; it iterates until no new addresses
are marked. The inner Propagate iterates over all addresses: for each address that
is itself marked, all of its children are marked. (Specifications of Mark-kids are in
Sections 3.4 and 4.3.)
Mark △
repeat
mc ← card marked;
Propagate
until card marked = mc
Propagate △
consid ← { };
do while consid 6= Addr
let x ∈ (Addr − consid) in
if x ∈ marked then Mark-kids(x) else skip;
consid ← consid ∪ {x}
od
Fig. 1. Code for Mark
In the case when the code is running with no interference, R/G reasoning is not
required and the specification of Mark and proof that the code in Fig. 1 satisfies that
specification are straightforward. (In fact, they are simplified cases of what follows
in Section 3.2.) When the same code is considered in the interfering environments
in Sections 3.2 and 4, (differing) R/Gs and, of course, proofs are needed. The elaboration of the R/Gs is particularly interesting.
Lesson 6 Considering the sequential case is useful because it is then possible to note
how the rely condition (nothing changes) and the guarantee condition (true) need to
be changed to handle concurrency.
3.2 Concurrent GC with atomic interference
The complication in the concurrent case is that the Mutator can interfere with the
marking strategy of the Collector by redirecting pointers. This can be accommodated
providing the Mutator marks appropriately whenever it makes a change.
The development is tackled in two stages: firstly, this section assumes a Mutator
that atomically both redirects a pointer in a Node and marks the new address; Section 4 shows that even separating the two steps still allows the Collector code of
Fig. 1 to achieve the lower bound of marking but the argument is more delicate and
indicates an expressive limitation of R/G conditions. The argument to establish the
upper bound for marking (and thus the lower bound of garbage collection) is given
in Section 5.
If the Mutator were able to update and mark atomically, specifications and proofs
are straightforward; although this atomicity assumption is unrealistic, it is informative to compare this section with Section 4. As adumbrated in Section 1, the argument is split into a justification of the parallel decomposition (Section 3.3) and the
decompositions of the Collector/Mutator sub-components, addressed in Sections 3.4
and 3.5 respectively.
3.3 Parallel decomposition
An R/G specification of the Collector is:
Collector
ext wr free, marked
rd roots, hp
pre true
rely free′ ⊆ free ∧ marked ⊆ marked′ ∧
∀(a, i) ∈ dom hp ·
hp′ (a, i) 6= hp(a, i) ∧ hp′ (a, i) ∈ Addr ⇒ hp′ (a, i) ∈ marked′
guar free ⊆ free′
S˜
lower bound for GC
post (Addr − reach(roots, hp)) ⊆ free
Here again, the notation for possible values is used to cover interference.
The final conjunct of the rely condition is the key property that (for now) assumes
that the environment (i.e. the Mutator) simultaneously marks any change it makes
to the heap.5
The lower bound of addresses to be collected is one part of the requirement; the
upper bound is constrained by the second conjunct of inv-Σ2 . The lower bound for
garbage collection requires setting an upper bound for marking addresses; this topic
is postponed to Section 5.
Lesson 7 Such splitting of what would be an equality in the specification of a sequential component is a common R/G tactic.
The corresponding specification of the Mutator is:
Mutator
ext wr hp, free, marked
rd roots
pre true
rely free ⊆ free′
guar free′ ⊆ free ∧ marked ⊆ marked′ ∧
∀(a, i) ∈ dom hp ·
hp′ (a, i) 6= hp(a, i) ∧ hp′ (a, i) ∈ Addr ⇒ hp′ (a, i) ∈ marked′
post true
The R/G proof obligation (PO) for concurrent processes requires that each one’s
guarantee condition implies the rely condition of the other(s); in this case they
match identically so the result is immediate.
5
Strictly, the fact that the Collector (in particular, its Sweep component) does not have write
access to hp means that it cannot clean up the nodes in free as required by the final conjunct
of inv-Σ2 . Changing the guarantee conditions is routine but uninformative.
3.4 Developing the Collector code
As outlined in Section 1, what remains to be done is to develop code that satisfies the
specification of the Collector (in isolation from that of the Mutator) — i.e. show that
the decomposition of the Collector into three phases given in Section 3.1 satisfies the
Collector specification in Section 3.3 and then to develop code for Mark.
A post condition for a sequential version of Unmark could constrain marked′ to
be exactly equal to roots ∪ free but, again, interference must be considered. The rely
condition indicates that the environment can mark addresses so whatever Unmark
removes from marked could be replaced. The possible values notation is again deployed so that post-Unmark requires that, for every address which should not be
marked, a possible value of marked exists which does not contain the address. However, this post condition alone would permit an implementation of Unmark itself first
to mark an address and then remove the marking; this erroneous behaviour is ruled
out by guar-Unmark. The rely condition indicates that the free set can also change
but, since it can only reduce, this poses no problem.
Unmark
ext wr marked
rd roots, free
pre true
rely free′ ⊆ free
guar marked′ ⊆ marked
˚·a∈
post ∀a ∈ (Addr − (roots ∪ free)) · ∃m ∈ marked
/m
The relaxing of the post condition again uses the idea in Lesson 7.
The post condition for Mark also has to cope with the interference absent from
a sequential specification and this requires more thought. In the sequential case,
post-Mark could use a strict equality to require that all reachable nodes are added to
marked but here the equality is split into a lower and upper bound. The lower bound
for marking is crucial to preserve the upper bound of garbage collection (see the
second conjunct of inv-Σ2 ). This lower bound is recorded in the post condition. (The
use of hp′ is, of course, challenging but the post condition is stable [CJ07,WDP10]
under the rely condition.) The “loss” (from the equality in the sequential case) of
the other containment is compensated for by setting an upper bound for marking
(see no-mog in Section 5).
Mark
ext wr marked
rd roots, hp, free
pre true
rely rely-Collector
guar marked ⊆ marked′
post reach(marked, hp′ ) ⊆ marked′
The relaxing of the post condition once again uses the idea in Lesson 7. Similar
observations to those for Unmark relate to the specification of Sweep which, for the
concurrent case, becomes:
Sweep
ext wr free
rd marked
pre true
rely free′ ⊆ free ∧ marked ⊆ marked′
guar free ⊆ free′
post (free′ − free) ∩ marked = { } ∧
˜·a∈f
∀a ∈ (Addr − marked) · ∃f ∈ free
The rely and guarantee conditions of Collector are distributed (with appropriate
weakening/strengthening) over the three sub-components; all of the pre conditions
are true; so the remaining PO is:
post-Unmark(σ, σ′ ) ∧ post-Mark(σ′ , σ′′ ) ∧ post-Sweep(σ′′ , σ′′′ ) ⇒
post-Collector(σ, σ′′′ )
The proof is straightforward.
Turning to the decomposition of Mark (see Fig. 1), in order to prove post-Mark,
a specification is needed for Propagate that copes with interference:
Propagate
ext wr marked
rd hp
pre true
rely rely-Collector
guar S
marked ⊆ marked′
post {elems hp′ (a) ∩ Addr | a ∈ marked} ⊆ marked′ ∧
(marked ⊂ marked′ ∨ reach(roots, hp′ ) ⊆ marked′ )
The first conjunct of the post condition indicates the progress required of the wave
of marking; the second triggers further iterations if any marking has occurred.
To prove the lower marking bound (i.e. must mark everything that is reachable
from roots), we use an argument that composes on the right a relation that expresses
the rest of the computation as in [Jon90]: essentially the to-end relation states that
the remaining iterations of the loop will mark everything reachable from what is
already marked:
to-end(σ, σ′ )
△
reach(marked, hp′ ) ⊆ marked′
The PO is:
post-Propagate(σ, σ′ ) ∧ σ.marked ⊂ σ′ .marked ∧ to-end(σ′ , σ′′ ) ⇒
to-end(σ, σ′′ )
whose proof is straightforward.
The termination argument follows from there being a limit to the markable elements: a simple upper bound is dom hp but there is a tighter one (cf. Section 5).
Then trivially:
σ.marked = σ.roots ∧ to-end(σ, σ′ ) ⇒ post-Mark(σ, σ′ )
Pursuing the decomposition of Propagate (again, see Fig. 1) needs a specification
of the inner operation:
Mark-kids (x: Addr)
ext wr marked
rd hp
pre true
rely rely-Collector
guar marked ⊆ marked′
post (elems hp′ (x) ∩ Addr) ⊆ marked′
In this case, the proof is more conventional and a relation that expresses how
far the marking has progressed is composed on the left:
′
△
so-far(σ,
S σ)
{elems hp(a) ∩ Addr | a ∈ (marked ∩ consid′ )} ⊆ marked′
The relevant PO is:
so-far(σ, σ′ ) ∧
consid′ 6= Addr∧post-Mark-kids(σ′ , x, σ′′ )∧consid′′ = consid′ ∪{x} ⇒
so-far(σ, σ′′ )
whose discharge is obvious.
The final obligation is to show:
so-far(σ, σ′ ) ∧ consid′ = Addr ⇒ post-Propagate(σ, σ′ )
The first conjunct of post-Propagate is straightforward; the fact that (unless the marking process is complete) some marking must occur in this iteration of Propagate
follows from the lemma in Section 2.2.
3.5 Checking the interference from Mutator
The mutator is viewed as an infinite loop non-deterministically selecting one of
Redirect, Malloc, Zap as specified below. At this stage, these are viewed as atomic
operations so no R/Gs are supplied here:6 their respective post conditions must be
shown to imply rely-Mark):
Redirect (a: Addr, i: N1 , b: Addr)
ext wr hp, marked
pre {a, b} ⊆ reach(roots, hp) ∧ i ∈ inds hp(a)
post hp′ = hp † {(a, i) 7→ b} ∧ marked′ = marked ∪ {b}
It follows trivially that:
post-Redirect(σ, σ′ ) ⇒ guar-Mutator(σ, σ′ )
For this atomic case, the code (using multiple assignment) would be:
< hp(a), marked : = hp(a) † {i 7→ b}, marked ∪ {b} >
Malloc (a: Addr, i: N1 , b: Addr)
ext wr hp, free
pre a ∈ reach(roots, hp) ∧ i ∈ inds hp(a) ∧ b ∈ free
post hp′ = hp † {(a, i) 7→ b} ∧ free′ = free − {b}
Malloc preserves the invariant because inv-Σ2 insists that free addresses are always
marked. It follows trivially that:
post-Malloc(σ, σ′ ) ⇒ guar-Mutator(σ, σ′ )
Zap (a: Addr, i: N1 )
ext wr hp
pre a ∈ reach(roots, hp) ∧ i ∈ inds hp(a)
post hp′ = hp † {(a, i) 7→ nil}
It again follows trivially that:
post-Zap(σ, σ′ ) ⇒ guar-Mutator(σ, σ′ )
4
Relaxing atomicity: reasoning using a ghost variable
The interesting challenge remaining is to consider the impact of acknowledging that
the atomicity assumption in Section 3.2 about the mutator is unrealistic. Splitting
the atomic assignment (on the two shared variables hp, marked) in Section 3.5 is
delicate. The reader would be excused for thinking that performing the marking
first would be safe but there is a counter example in the case where the Collector
executes Unmark between the two steps of such an erroneous Redirect.
For the general lessons that this example illustrates, the interesting conclusion
is that there appears to be no way to maintain full compositionality (i.e. expressing
all we need to know about the mutator) with standard rely conditions.
6
The all-important non-atomic case for Redirect is covered in Section 4.
Lesson 8 Ghost variables can undermine compositionality/separation (cf. Lesson 3).
Where they appear to be essential, it would be useful to have a test for this fact.
The difficulty can be understood by considering the following scenario. Redirect
can, at the point that it changes hp(a, i) to point to some address b, go to sleep
before performing the marking on which the Collector of Section 3.4 relies. There is
in fact no danger since, even if b was not marked, there must be another path to b
(see pre-Redirect in Section 3.5) and the Collector should perform the marking when
that path (say hp(c, j)) is encountered. If, however, that hp(c, j) could be destroyed
before the Collector gets to c, an incomplete marking would result that could cause
live addresses to be collected as garbage. What saves the day is that the Mutator
cannot make another change without waking up and marking b.7
It is precisely this three step argument that pinpoints the limitation of using two
state relations in R/G reasoning.
Here, despite all of the reservations expressed in Section 1, a ghost variable is
employed to complete the justification
of the design;thus the state Σ2 is extended
with a ghost variable tbm: Addr that can record an address as “to be marked”.
Lesson 9 Lesson 8 asks for a test that would justify the use of ghost variable; the need
for a “3-state” justification is such a test.8
4.1 Parallel decomposition
The rely condition used in Section 3.3 is replaced for the non-atomic interference
from the mutator by:
rely-Collector : Σ2 × Σ2 → B
rely-Collector(σ, σ′ ) △
free′ ⊆ free ∧ marked ⊆ marked′ ∧
(∀(a, i) ∈ hp ·
hp′ (a, i) 6= hp(a, i) ∧ hp′ (a, i) ∈ Addr ⇒
tbm′ = hp′ (a, i) ∨ hp′ (a, i) ∈ marked′ ) ∧
′
(tbm 6= nil ∧ tbm 6= tbm ⇒ tbm ∈ marked′ ∧ tbm′ = nil)
Here again, the PO of the parallel introduction rule is trivial to discharge because
the guarantee condition of the Mutator is identical with the rely condition of the
Collector.
7
8
This line of argument rules out multiple Mutator threads.
A planned journal version of this paper will investigate other options. It is also hoped to
compare the approaches with RGITL [STER11].
4.2 Developing Mutator code
As indicated in Section 1, it still remains to establish that the design of each component satisfies its specification. Looking first at the non-atomic Mutator argument,
the only real challenge is:9
Redirect (a: Addr, i: N1 , b: Addr)
ext wr hp, marked
pre {a, b} ⊆ reach(roots, hp) ∧ i ∈ inds hp(a)
rely hp′ = hp
guar rely-Collector
post hp′ = hp † {(a, i) 7→ b} ∧ b ∈ marked′
Redirect can satisfy this specification by executing the following two atomic steps
(but the atomic brackets only surround one shared variable in each case):
< hp(a), tbm : = hp(a) † {i 7→ b}, b >;
< marked, tbm : = marked ∪ {b}, nil >
This not only guarantees rely-Collector, but also preserves the following invariant:
tbm 6= nil ⇒ ∃{(a, i), (b, j)} ⊆ dom hp·(a, i) 6= (b, j)∧hp(a, i) = hp(b, j) = tbm
4.3 Developing Collector code
Turning to the development of Collector, code must be developed relying only on the
above interface. The only challenge is the mark phase whose specification is:
Mark
ext wr marked
rd roots, hp, free
pre true
rely rely-Collector
guar marked ⊆ marked′
post reach(marked, hp′ ) ⊆ marked′
The code for Mark is still that in Fig. 1 — under interference, the post condition
of Propagate has to be further weakened (from Section 3.4) to reflect that, if there is
an address in tbm, its reach might not yet be marked. Importantly, if the marking is
not yet complete, there must have been some node marked in the current iteration:
9
When removing a pointer, no tbm is set — see Zap(a, i) in Section 3.5; also no tbm is needed
in the Malloc case because inv-Σ2 ensures that any free address is marked.
Propagate
ext wr marked
rd hp
pre true
rely rely-Collector
guar S
marked ⊆ marked′
post {elems hp′ (a) ∩ Addr | a ∈ marked} ⊆ (marked′ ∪ ({tbm′ } ∩ Addr)) ∧
(marked ⊂ marked′ ∨ reach(roots, hp′ ) ⊆ marked′ )
Notice that post-Propagate implies there can be at most one address whose marking
is problematic; this fact must be established using the final conjunct of rely-Collector.
The correctness of this loop is interesting — it follows the structure of that in
Section 3.4 using a to-end relation and, in fact, the relation is still:
to-end(σ, σ′ )
△
reach(marked, hp′ ) ⊆ marked′
The PO is now:
post-Propagate(σ, σ′ ) ∧ σ.marked ⊂ σ′ .marked ∧ to-end(σ′ , σ′′ ) ⇒
to-end(σ, σ′′ )
In comparison with the PO in Section 3.4, the difficult case is where tbm′ 6= nil (in
the converse case the earlier proof would suffice). What needs to be shown is that
the stray address in tbm′ will be marked. The lemma in Section 4.2 ensures there
is another path to the address in tbm′ ; this will be marked if there are further iterations of Propagate and these are ensured by the lemma at the end of Section 2.2
which, combined with the second conjunct of post-Propagate, avoids premature termination.
The code in Fig. 1 shows how Propagate uses Mark-kids in the inner loop.
Mark-kids (x: Addr)
ext wr marked
rd hp
pre true
rely rely-Collector
guar marked ⊆ marked′
post (elems hp′ (x) ∩ Addr) ⊆ (marked′ ∪ ({tbm′ } ∩ Addr)))
Again, the POs are as for the atomic case, but with:
′
△
so-far(σ,
S σ)
{elems hp(a) ∩ Addr | a ∈ (marked ∩ consid′ )} ⊆
(marked′ ∪ ({tbm′ } ∩ Addr))
5
Lower limit of GC
Sections 3.2 and 4 address (under different assumptions) the lower bound for marking and thus ensure that no active addresses are treated as garbage. Unless an upper
bound for marking is established however, Mark could mark every address and no
garbage would be collected. The R/G technique of splitting, for example, a set equality into two containments often results in such a residual PO.
Addresses that were garbage in the initial state (Addr − (reach(roots, hp) ∪ free))
should not be marked (thus any garbage will be collected at the latest after two
passes of Collect). A predicate “no marked old garbage” can be used for the upper
bound of marking:
no-mog : Addr-set × Addr-set × Heap × Addr-set → B
no-mog(r, f , h, m)
△
(Addr − (reach(r, h) ∪ f )) ∩ m = { }
The intuitive argument is simple: the Collector and Mutator only mark things reachable from roots and the Mutator can change the reachable graph but only links to
addresses (from free or previously reachable from roots) that were never “garbage”.
6
Related work
The nine lessons are the real message of this paper; the (garbage collection) example illustrates and hopefully clarifies the issues for the reader. The current authors
believe that examples are essential to drive research.
Many papers exist on garbage collection algorithms, where the verification is
usually performed at the code level, e.g. [GGH07] and [HL10], which both use the
PVS theorem prover. In [TSBR08], a copying collector with no concurrency is verified using separation logic. An Owicki-Gries proof of Ben-Ari’s algorithm is given in
[NE00]; while this examines multiple mutators, the method results in a very large
number of POs. The proof of Ben-Ari’s algorithm in [vdS87], also using Owicki-Gries,
reasons directly at the code level without using abstraction.
Perhaps the closest to our approach is [PPS10], which presents a refinementbased approach for deriving various garbage collection algorithms from an abstract
specification. This approach is very interesting and for future work it is worth exploring how the approach given here could be used to verify a similar family of
algorithms. It would appear that the rely-guarantee method produces a more compositional proof, as the approach in [PPS10] requires more integrated reasoning
about the actions of the Mutator and Collector. Similarly, in [VYB06], a series of
transformations is used to derive various concurrent garbage collection algorithms
from an initial algorithm.
Acknowledgements
We have benefitted from productive discussions with researchers including José
Nuno Oliviera and attendees at the January 2017 Northern Concurrency Working
Group held at Teesside University. In particular, Simon Doherty pointed out that GC
is a nasty challenge for any compositional approach because the mutator/collector
were clearly thought out together; this is true but looking at an example at the fringe
of R/G expressivity has informed the notion of compositional development.
Our colleagues in Newcastle, Leo Freitas and Diego Machado Dias are currently
formalising proofs of the lemmas and POs using Isabelle.
The authors gratefully acknowledge funding for their research from EPSRC grant
Taming Concurrency.
References
[BA84]
Mordechai Ben-Ari. Algorithms for on-the-fly garbage collection. ACM Transactions on Programming Languages ans Systems, 6(3):333–344, 1984.
[BA10]
Richard Bornat and Hasan Amjad. Inter-process buffers in separation logic with
rely-guarantee. Formal Aspects of Computing, 22(6):735–772, 2010.
[BA13]
Richard Bornat and Hasan Amjad. Explanation of two non-blocking sharedvariable communication algorithms. Formal Aspects of Computing, 25(6):893–
931, 2013.
[BvW98] R.-J. R. Back and J. von Wright. Refinement Calculus: A Systematic Introduction.
Springer, New York, 1998.
[CJ00]
Pierre Collette and Cliff B. Jones. Enhancing the tractability of rely/guarantee
specifications in the development of interfering operations. In Gordon Plotkin,
Colin Stirling, and Mads Tofte, editors, Proof, Language and Interaction, chapter 10, pages 277–307. MIT Press, 2000.
[CJ07]
J. W. Coleman and C. B. Jones. A structural proof of the soundness of
rely/guarantee rules. Journal of Logic and Computation, 17(4):807–841, 2007.
[Col08]
Joseph William Coleman. Constructing a Tractable Reasoning Framework upon a
Fine-Grained Structural Operational Semantics. PhD thesis, Newcastle University,
January 2008.
[DFPV09] Mike Dodds, Xinyu Feng, Matthew Parkinson, and Viktor Vafeiadis. Denyguarantee reasoning. In Giuseppe Castagna, editor, Programming Languages
and Systems, volume 5502 of Lecture Notes in Computer Science, pages 363–377.
Springer Berlin / Heidelberg, 2009.
[Din00]
Jürgen Dingel. Systematic Parallel Programming. PhD thesis, Carnegie Mellon
University, 2000. CMU-CS-99-172.
[DYDG+ 10] Thomas Dinsdale-Young, Mike Dodds, Philippa Gardner, Matthew J. Parkinson,
and Viktor Vafeiadis. Concurrent abstract predicates. In Proceedings of the 24th European Conference on Object-Oriented Programming, pages 504–528, Berlin, Heidelberg, 2010. Springer-Verlag.
[FFS07]
Xinyu Feng, Rodrigo Ferreira, and Zhong Shao. On the relationship between concurrent separation logic and assume-guarantee reasoning. In ESOP: Programming
Languages and Systems, pages 173–188. Springer, 2007.
[GGH07] Hui Gao, Jan Friso Groote, and Wim H. Hesselink. Lock-free parallel and concurrent garbage collection by mark&sweep. Sci. Comput. Program., 64(3):341–374,
2007.
[HBDJ13] Ian J. Hayes, Alan Burns, Brijesh Dongol, and Cliff B. Jones. Comparing degrees
of non-determinism in expression evaluation. The Computer Journal, 56(6):741–
755, 2013.
[HJC14]
[HL10]
[Hoa72]
[JH16a]
[JH16b]
[JHC15]
[Jon81]
[Jon90]
[Jon96]
[JP11]
[JY15]
[Mor90]
[NE00]
[OG76]
[O’H07]
[Owi75]
[Par10]
[Pie09]
[PPS10]
I. J. Hayes, C. B. Jones, and R. J. Colvin. Laws and semantics for rely-guarantee
refinement. Technical Report CS-TR-1425, Newcastle University, July 2014.
Wim H. Hesselink and Muhammad Ikram Lali. Simple concurrent garbage
collection almost without synchronization. Formal Methods in System Design,
36(2):148–166, 2010.
C.A.R. Hoare. Towards a theory of parallel programming. In Operating System
Techniques, pages 61–71. Academic Press, 1972.
Cliff B. Jones and Ian J. Hayes. Possible values: Exploring a concept for concurrency. Journal of Logical and Algebraic Methods in Programming, 85(5, Part
2):972–984, August 2016. Articles dedicated to Prof. J. N. Oliveira on the occasion of his 60th birthday.
Cliff B. Jones and Ian J. Hayes. Possible values: Exploring a concept for concurrency. Journal of Logical and Algebraic Methods in Programming, 85(5, Part
2):972–984, August 2016.
C. B. Jones, I. J. Hayes, and R. J. Colvin. Balancing expressiveness in formal
approaches to concurrency. Formal Aspects of Computing, 27(3):475–497, May
2015.
C. B. Jones. Development Methods for Computer Programs including a Notion of Interference. PhD thesis, Oxford University, June 1981. Available as: Oxford University Computing Laboratory (now Computer Science) Technical Monograph PRG25.
C. B. Jones. Systematic Software Development using VDM. Prentice Hall International, second edition, 1990.
C. B. Jones. Accommodating interference in the formal design of concurrent
object-based programs. Formal Methods in System Design, 8(2):105–122, March
1996.
Cliff B. Jones and Ken G. Pierce. Elucidating concurrent algorithms via layers of
abstraction and reification. Formal Aspects of Computing, 23(3):289–306, 2011.
Cliff B. Jones and Nisansala Yatapanage. Reasoning about separation using
abstraction and reification. In Radu Calinescu and Bernhard Rumpe, editors,
Software Engineering and Formal Methods, volume 9276 of LNCS, pages 3–19.
Springer, 2015.
Carroll Morgan. Programming from Specifications. Prentice-Hall, 1990.
Leonor Prensa Nieto and Javier Esparza. Verifying single and multi-mutator
garbage collectors with Owicki-Gries in Isabelle/HOL. In MFCS 2000, volume
1893 of LNCS, pages 619–628. Springer, 2000.
S. S. Owicki and D. Gries. An axiomatic proof technique for parallel programs I.
Acta Informatica, 6(4):319–340, 1976.
P. W. O’Hearn. Resources, concurrency and local reasoning. Theoretical Computer
Science, 375(1-3):271–307, May 2007.
S. Owicki. Axiomatic Proof Techniques for Parallel Programs. PhD thesis, Department of Computer Science, Cornell University, 1975.
Matthew Parkinson. The next 700 separation logics. In Gary Leavens, Peter
O’Hearn, and Sriram Rajamani, editors, Verified Software: Theories, Tools, Experiments, volume 6217 of LNCS, pages 169–182. Springer, 2010.
Ken Pierce. Enhancing the Useability of Rely-Guaranteee Conditions for Atomicity
Refinement. PhD thesis, Newcastle University, 2009.
Dusko Pavlovic, Peter Pepper, and Douglas R. Smith. Formal derivation of concurrent garbage collectors. In MPC 2010, volume 6120 of LNCS, pages 353–376.
Springer, 2010.
[Pre01]
Leonor Prensa Nieto. Verification of Parallel Programs with the Owicki-Gries and
Rely-Guarantee Methods in Isabelle/HOL. PhD thesis, Institut für Informatic der
Technischen Universitaet München, 2001.
[STER11] G. Schellhorn, B. Tofan, G. Ernst, and W. Reif. Interleaved programs and relyguarantee reasoning with ITL. In TIME, pages 99–106, 2011.
[Stø90]
K. Stølen. Development of Parallel Programs on Shared Data-Structures. PhD thesis,
Manchester University, 1990. Available as UMCS-91-1-1.
[TSBR08] Noah Torp-Smith, Lars Birkedal, and John C. Reynolds. Local reasoning about a
copying garbage collector. ToPLaS, 30:24:1–24:58, July 2008.
[Vaf07]
V. Vafeiadis. Modular fine-grained concurrency verification. PhD thesis, University
of Cambridge, 2007.
[vdS87]
Jan L.A. van de Snepscheut. “Algorithms for on-the-fly garbage collection” revisited. Information Processing Letters, 24(4):211–216, 1987.
[VYB06] Martin T. Vechev, Eran Yahav, and David F. Bacon. Correctness-preserving derivation of concurrent garbage collection algorithms. In PLDI, pages 341–353, 2006.
[WDP10] J. Wickerson, M. Dodds, and M. J. Parkinson. Explicit stabilisation for modular
rely-guarantee reasoning. In A. D. Gordon, editor, ESOP, volume 6012 of LNCS,
pages 610–629. Springer, 2010.
[Xu92]
Qiwen Xu. A Theory of State-based Parallel Programming. PhD thesis, Oxford
University, 1992.