A Programming Model For Concurrent Object-Oriented Programs
A Programming Model For Concurrent Object-Oriented Programs
Object-Oriented Programs
BART JACOBS, FRANK PIESSENS, and JAN SMANS
Katholieke Universiteit Leuven
K. RUSTAN M. LEINO and WOLFRAM SCHULTE
Microsoft Research
Reasoning about multithreaded object-oriented programs is difficult, due to the non-local nature
of object aliasing and data races. We propose a programming regime (or programming model) that
rules out data races, and enables local reasoning in the presence of object aliasing and concurrency.
Our programming model builds on the multithreading and synchronization primitives as they
are present in current mainstream programming languages. Java or C# programs developed
according to our model can be annotated by means of stylized comments to make the use of
the model explicit. We show that such annotated programs can be formally verified to comply
with the programming model. If the annotated program verifies, the underlying Java or C#
program is guaranteed to be free from data races, and it is sound to reason locally about program
behavior. Verification is modular: a program is valid if all methods are valid, and validity of
a method does not depend on program elements that are not visible to the method. We have
implemented a verifier for programs developed according to our model in a custom build of the
Spec# programming system, and we have validated our approach on a case study.
Categories and Subject Descriptors: D.1.3 [Programming Techniques]: Concurrent Programming; D.1.5 [Programming Techniques]: Object-oriented Programming; D.2.4 [Software Engineering]: Software/Program VerificationClass invariants; Correctness proofs; Formal methods; Programming by contract; F.3.1 [Logics and Meanings of programs]: Specifying and
Verifying and Reasoning about ProgramsAssertions; Invariants; Logics of programs; Mechanical verification; Pre- and post-conditions; Specification techniques
General Terms: Verification
Additional Key Words and Phrases: Aliasing, data races, local reasoning, modular reasoning,
ownership, verification condition generation
1.
INTRODUCTION
c ACM, 2008. This is the authors version of the work. It is posted here by permission of
ACM for your personal use. Not for redistribution. The definitive version was published in ACM
Transactions on Programming Languages and Systems, Vol. 31, No. 1, December 2008.
http://doi.acm.org/10.1145/1452044.1452045
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008, Pages 147. (PREPRINT)
if an object is intended to be shared, and as a consequence it is practically impossible for the compiler or other static analysis tools to verify if locking is performed
correctly.
We propose a programming regime (or programming model ) for concurrent programming in Java-like languages, and the design of a set of program annotations
that make the use of the programming model explicit. For instance, a developer
can annotate their code to make explicit whether an object is intended to be shared
with other threads or not. These annotations provide sufficient information to static
analysis tools to verify if locking is performed correctly: shared objects must be
locked before use, unshared objects can only be accessed by the creating thread.
Moreover, the verification can be done modularly, hence verification scales to large
programs. We describe a particular modular verification approach, based on generating verification conditions suitable for discharge by an automatic theorem prover.
Several other approaches exist to verify race- and deadlock-freedom for multithreaded code. They range from generating verification conditions [Detlefs et al.
1998; Flanagan et al. 2002; Freund and Qadeer 2004; Flanagan et al. 2005; Qadeer
In this section, we present our programming model and associated modular static
verification approach for verification of the absence of data races in Java-like programs.
A data race occurs when multiple threads simultaneously access the same variable, and at least one of these accesses is a write access. Data races are usually
programming errors, since they are a symptom of a lack of synchronization between
concurrent operations on a data structure. Developers can protect data structures
accessed concurrently by multiple threads by associating a mutual exclusion lock
with each data structure and ensuring that a thread accesses the data structure only
when it holds the associated lock. However, mainstream programming languages
such as Java and C# do not force threads to acquire any locks before accessing data
structures, and they do not enforce that locks are associated with data structures
consistently.
A simple strategy to prevent data races is to lock every object before accessing
it. Although this approach is safe, it is not used in practice since it incurs a major
performance penalty, is verbose, and is prone to deadlocks. Instead, standard
practice is to only lock the objects that are effectively shared between multiple
threads. However, it is hard to distinguish shared objects (which should be locked)
from unshared objects based on the program text. As a consequence, without
additional annotations, a compiler cannot enforce a locking discipline where shared
objects can only be accessed when locked.
An additional complication is that in order to decide whether a particular field
access is correctly protected by a lock, it is not sufficient to inspect the method
that contains the field access; indeed, the lock that protects the field might be
acquired by the methods direct or transitive callers instead of by the method
itself. Therefore, method contracts that specify which locks are held on entry are
required for modular verification.
In this section, we describe a simple version of our approach that deals with
data races on the fields of shared objects. The next section develops this approach
further to deal with high-level races on multi-object data structures.
The approach is presented in two steps. First (Section 2.1), a programming
regime (or programming model ) that prevents data races is presented without regard
to modular static verification. It is proven that if a program complies with the
programming model, then it is data-race-free. In the second step (Section 2.2), the
modular static verification approach is presented.
2.1
Programming model
This section presents the programming model, without regard to modular static
verification. Section 2.1.1 describes the model informally. Section 2.1.2 formalizes
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
the syntax for a subset of Java augmented with the annotations required by the
approach. Section 2.1.3 defines a small step semantics for execution of programs in
this language, which tracks the extra state variables (specifically, access sets and
shared sets) required by the programming model. The section also defines a set
of legal program states that comply with the programming model. Section 2.1.4
proves that programs that reach only legal program states are data-race-free.
2.1.1 Informal Description. We describe our programming model in the context
of Java, but it applies equally to C# and other similar languages. In the following
sections, we formalize the approach with respect to a formally defined core subset
of Java.
In our programming model, accesses to shared objects are synchronized using
Javas synchronized statement. A thread may enter a synchronized (o) block
only if no other thread is executing inside a synchronized (o) block; otherwise,
the thread waits. In the remainder of the paper, we use the following terminology
to refer to Javas built-in synchronization mechanism: when a thread enters a
synchronized (o) block, we say it acquires os lock or, as a shorthand, that it
locks o; while it is inside the block, we say it holds os lock ; and when it exits
the block, we say it releases os lock, or, as a shorthand, that it unlocks o. Note
that, contrary to what the terminology may suggest, when a thread locks an object,
the Java language prevents other threads from locking the object but it does not
prevent other threads from accessing the objects fields. This is the main problem
addressed by the proposed methodology. While a thread holds an objects lock, we
also say that the object is locked by the thread.
An important terminological point is the following: when a thread tids program
counter reaches a synchronized (o) block, we say the thread attempts to lock o.
Some time may pass before the thread locks o, specifically if another thread holds os
lock. Indeed, if the other thread never unlocks o, tid never locks o. The distinction
is important because our programming model imposes restrictions on attempting
to lock an object.
Our programming model prevents data races by ensuring that no two threads
have access to a given object at any one time. Specifically, it conceptually associates
with each thread an access set, which is the set of objects whose fields the thread
is allowed to read or write at a given point, and the model ensures that no two
threads access sets ever intersect. Access sets can grow and shrink when objects are
created, objects are shared, threads are created, or when a thread enters or exits a
synchronized block. Note that these access sets do not influence program behavior
and therefore need not be tracked by a virtual machine implementation: we use
them to explain the programming model, and to implement the static verification.
Object creation. When a thread creates a new object, the object is added to
the creating threads access set. This means the constructor can initialize the
objects fields without acquiring a lock first. This also means single-threaded
programs just work: if there is only a single thread, it creates all objects, and it
can access them without locking.
Object sharing. In addition to access sets, our model introduces an additional
global state variable called the shared set, which is a set of object identities. We
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
shared
new
lock
share
unshared
free
locked
unlock
Fig. 1.
call the objects in the shared set shared and objects in the complement unshared.
The shared set, like the access sets, is conceptual: it is not present at run time,
but used to explain the model and implement the verification.
A new object is initially unshared. Threads other than the creating thread are
not allowed to access its fields. In addition, no thread is allowed to attempt to
lock an unshared object: our programming model does not allow a synchronized
(o) {...} operation unless o is shared. In our programming model, objects that
are not intended to be shared are never locked.
If, at some point in the code, the developer wants to make the object available for
concurrent access, they have to indicate this through an annotation (the share
o; annotation). From that point on, the object o is shared, and threads can
attempt to acquire the objects lock. At the point where an object is shared, the
object is removed from the creating threads access set and added to the shared
set. If, subsequent to this transition, any thread, including the creating thread,
wishes to access the object, it must acquire its lock first.
Once shared, an object can never revert to the unshared state.
Thread creation. Starting a new thread transfers the accessibility of the receiver object of the threads main method (i.e. the Runnable object in Java, or
the ThreadStart delegate instances target object in the .NET Framework) from
the starting thread to the started thread. This is necessary since otherwise, the
threads main method would not be allowed to access its receiver.
Acquiring and releasing locks. At the point where an object becomes shared,
it is removed from the creating threads access set and added to the shared set.
Since the object is now not part of any threads access set, no thread is allowed
to access it. To gain access to such a shared object, a thread must lock the object
first. When a thread locks an object, the object is added to the threads access
set for the duration of the synchronized block.
As illustrated in Figure 1, an object can be in one of three states: unshared ,
free (not locked by any thread and shared) or locked (locked by some thread and
shared). Initially, an object is unshared. Some objects eventually undergo a share
operation (at a program point indicated by the developer). After this operation,
the object is not part of any threads access set and is said to be free. To access
a free object, it must be locked first, changing its state to locked and adding the
object to the locking threads access set. Unlocking the object removes it from the
access set and makes it free again.
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
Lets summarize. Threads are only allowed to access objects in their corresponding access set. A threads access set consists of all objects it created or whose lock
it acquired, plus the receiver of its main method (if any), minus the objects on
which it subsequently performed a share operation or which it used as the target
in a thread creation, or whose lock it released. Our programming model prevents
data races by ensuring that access sets never intersect.
2.1.1.1 Lock re-entry. Javas synchronized blocks are re-entrant; that is, if a
thread already holds an object os lock, then another, nested attempt by the same
thread to enter a synchronized (o) block succeeds immediately. For simplicity, we
rule out lock re-entry in the programming model. However, in Section 4, we show
how our approach can be extended to support lock re-entry.
2.1.2 Programs. To formalize the rules imposed by our programming model, we
first define a small language consisting of a subset of Java (minus static typing) plus
two kinds of annotations (indicated by the gray background): share statements and
method contracts. Its syntax is shown in Figure 2. An example program in this
language is shown in Figure 3. We discuss the language and define well-formedness
of programs.
C
I
class names
interface names
M
F
X
method names
field names
variable names
logical formulae
object references
thread-relevant states
C C, I I, m M, f F , x X , , o O,
iface
mh
v
g
class
field
meth
::=
::=
::=
::=
::=
::=
::=
::=
::=
::=
hiface | classi s
interface I { mh }
m(x ) requires ; ensures ;
null | o
this | x | v
class C implements I { field meth }
f;
mh { s }
C |I
if (g = g) { s } else { s } | assert g instanceof ;
| x := g.f ; | g.f := g; | x := new C; | x := g.m(g ); | start g.m();
| share g; | synchronized (g) { s }
| return g; | x := receive [] o.m(v ); | unlock o;
Fig. 2. Syntax of a small Java-like language without static typing, but with two kinds of annotations (indicated by the gray background): method contracts and share statements. The underlined
elements appear only as part of continuations during program execution (see Section 2.1.3) and
are not allowed to appear in well-formed programs. The syntax of the logical formulae used in
method contracts is given in Section 2.2.
Fig. 3. An example program in the formal syntax of Figure 2. (Note: the example also uses integer
values and integer operations; these are omitted from the formal development for simplicity.)
Method runs precondition states that the threads lockset is empty and that the receiver is in
the threads access set. The syntax and semantics of method contracts is detailed in Section 2.2.
method contract, consisting of a precondition (requires clause) and a postcondition (ensures clause). The precondition and postcondition specify an assertion
that must hold in the pre-state and the post-state of method calls, respectively.
Since method contracts are used only for verifying modularly whether a given program complies with the programming model, and are not part of the programming
model itself, it is safe to ignore them in this section.
In addition to method contracts, the syntax supports a second type of annotations, namely share statements. Using share statements, a programmer indicates
when an object transitions from unshared to shared.
The language is not statically typed. Rather, to avoid formalizing a type system,
type mismatches are considered run-time errors. The static verification approach
detailed in Section 2.2 guarantees the absence of programming model violations as
well as run-time errors (i.e., null dereferences and type mismatches).
For simplicity, the language does not include subclassing (i.e., Javas extends
keyword). However, it does include dynamic binding. A program with subclassing
could be encoded using delegation. Since the language also does not include inheritance among interfaces, the subtype relation is very simple: a type 1 is a subtype
of a type 2 if either 1 = 2 or 1 is a class and 2 is an interface and 1 mentions
2 in its implements clause.
Our static verification approach performs modular verification. This means that
for each method, all executions of that method are considered, not just those that
occur in executions of the program. For example, for method run of class Session
in Figure 3, all executions that satisfy runs precondition are considered, including
those where the count field contains a null value, even though there is no execution
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
of the program of Figure 3 where the method is called in a state where count is
null. It follows that in the absence of the assert statement, the method would
be considered invalid since in an execution where count is null, the synchronized
statement would cause a null dereference. This limits the usefulness of the approach,
since users are not interested in errors that do not occur in program executions.
To alleviate this limitation, the programmer may restrict the set of executions
considered by the static verification approach, by inserting assert statements into
the program. If an assert statements condition evaluates to false, the statement
is said to fail. If in a given execution an assert statement fails, the execution
is stuck (in Java, an exception is thrown), but for purposes of static verification,
the execution is considered to be valid (or in other words, the execution is not
considered further), since the failure is assumed to mean that the method execution
never appears in an execution of the whole program. Other verification approaches
outside the scope of this article, such as code review, model checking, or testing,
may be used to verify such assumptions. (Note that the assert statement of this
article, like the assert statement introduced in Java 1.4 and the assert macro in C,
is a run-time assertion, rather than a verification-time assertion. In this unfortunate
clash between the syntax of Java and that of Spec# [Barnett et al. 2004], where
run-time assertions are denoted using the assume keyword and verification-time
assertions using the assert keyword, we adhere to Java syntax.)
Note: in this article, we allow only assert statements of the form
assert g instanceof ;
which assert that g is a non-null object reference of type . We do not allow
arbitrary expressions because the provided form is functionally complete and avoids
the need to formalize expression syntax and semantics.
We only consider well-formed programs. Well-formed programs have no name
clashes. To simplify the formalisation, we require even fields and methods declared
in different classes or interfaces to have different names, except when a class method
implements an interface method. Also, local variables need not be declared before
they are assigned (in fact, our language does not have local variable declarations)
but they must be assigned before they are used. By requiring that both branches
of a conditional statement assign to the same variables, we can define a notion
of free variables at each program point by considering an assignment to bind the
variable occurrences that occur after it in the control flow and that are not hidden
by a later assignment. Further, a well-formed program must not contain any of
the syntactic forms that are intended to appear only during program execution
(i.e., the constructs underlined in Figure 2). Lastly, classes must implement all
methods of their declared interfaces, and if a method is used to start a new thread,
its precondition must be as prescribed below.
Definition 1. A program is well-formed if all of the following hold:
No two interfaces have the same name. No two classes have the same name. No
interface has the same name as a class. No two parameters of a given method
have the same name. No two fields have the same name (even if they appear in
different classes). No two interface methods have the same name (even if they
appear in different interfaces). If two class methods have the same name m, then
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
the methods are in different classes and the program declares an interface I that
declares a method with name m and both classes implement interface I.
If one branch of an if statement contains an assignment to a variable x, then so
does the other branch.
If a statement uses a variable x, then an assignment to x appears before the statement in the method body (ignoring the other branch of an enclosing if statement),
or x is a parameter of the enclosing method or this.
The last statement of a method body and of the main routine is a return statement and a return statement does not appear anywhere else.
The program does not contain any receive or unlock statements or object references.
If a class implements an interface I and this interface declares a method with
name m then the class declares a method with name m and its header is identical
to the header of the method named m declared by interface I.
Each class name, interface name, field name, and method name that appears in
the program is declared by the program.
The number of arguments specified in a call equals the number of parameters
declared by the corresponding method.
If a method is mentioned in a start statement, then its requires clause is exactly
Lt = this A. (Note: the semantics of method contracts is defined in
Section 2.2.)
Notice that the example program of Figure 3 is well-formed.
All concepts in the remainder of this paper are implicitly parameterized by a
program .
2.1.3 Program executions. We now formalize the semantics of our annotated
Java subset. While remaining faithful to Java semantics, our semantics additionally
tracks the extra state variables (specifically, access sets and shared sets) required
by the programming model. We also define a set of legal program states. A program
state is legal if all thread states are legal. A thread is in a legal state if it is not about
to violate the programming model or cause a run-time error (in particular, a null
dereference or a type mismatch). In Section 2.1.4, we show that programs that reach
only legal states are data-race-free. Note: we define legality as a separate judgment
rather than encoding it as absence of progress (i.e., thread execution getting stuck)
since in concurrent executions, on the one hand stuckness of a thread might not be
due to an error in that thread (for example, if the thread is waiting for a lock that
is never released), and on the other hand a thread that is stuck due to an error
might become un-stuck again as a result of actions of other threads (for example, if
the thread is attempting to lock an unshared object and the object is subsequently
shared by another thread).
We use the following notation. T denotes the set of thread identifiers. Furthermore, v represents a value (i.e. v O {null}) and o represents an object reference
(i.e., a non-null value). fields(C) represents the set of fields declared by class C.
We use C I to denote that class C implements interface I. declaringClass(f )
denotes the name of the class that declares field f , and declaringType(m) denotes
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
10
the name of the interface that declares method m, or the name of the class that
declares m if no interface declares a method m. Also, we assume the existence of
a function classof that maps object references to class names. We assume that for
each class name C C, there are infinitely many object references o O such that
classof(o) = C. mbody(o.m(v)) denotes the body of method m declared in class
classof(o), with o substituted for this and v substituted for the methods parameters. objectRefs() and free() denote the free object references and free variables,
respectively, in syntactic entity . We use f [x 7 y] to denote the update of the
function f at argument x with value y. Specifically, f [x 7 y](z) equals y if z = x
and f (z) otherwise. Similarly, f \ {(x, y)} removes the mapping of x to y from f ,
and thereby removes x from the domain of f . We use the notation s[v/x] to denote
substitution of a value v for a program variable x in a thread continuation (i.e.,
sequence of statements) s. This substitution replaces only the free occurrences of x,
i.e., the ones that are not bound by an assignment inside s. We denote the empty
list as and a list with head h and tail t as h t. As in Java, we use juxtaposition
to denote the concatenation of two statements or sequences of statements.
The dynamic semantics is defined as a small step relation on program states.
Definition 2. A program state
= (H, L, S, T)
consists of:
the heap H, a partial function that maps object references to object states. An
object state is a partial function that maps field names to values.
H : O , (F , O {null})
The domain of H consists of all allocated objects. The domain of an object state
H(o) consists of the declared fields of the class classof(o) of o.
the lock map L, a partial function that maps a locked object to the identifier of
the thread that holds the lock
L : O , T
the shared set S, the set of shared objects
the thread set T. Each thread state (tid, A, F) T consists of a unique thread
identifier tid T , an access set A O and a list of activation records F (s ) .
We shall sometimes use uncurried syntax for the heap: H(o, f ) is shorthand for
H(o)(f ), and H[(o, f ) 7 v] is shorthand for H[o 7 H(o)[f 7 v]].
Figure 4 shows the definition of legality H, L, S ` t : legal of a thread state t with
respect to heap H, lock map L, and shared set S. Legality captures the rules of the
programming model, as well as absence of run-time errors (i.e., null dereferences
and type mismatches). Figure 5 shows the definition of the small step relation
on program states.
The rule If is standard. An assert statement that fails (either because the
operand is null or because it is not of the specified type) causes the thread to block
forever (Assert). For reading (Read) or writing (Write) a field f , the target
object must be non-null, part of the current threads access set, and of the class
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
11
v 6= null
[Legal-Read]
vA
classof(v) = declaringClass(f )
v1 6= null
[Legal-Write]
v1 A
classof(v1 ) = declaringClass(f )
v 6= null
[Legal-Share]
v 6= null
[Legal-Synchronized]
vA
v 6 S
vS
[Legal-Unlock]
v 6= null
[Legal-Call]
(o, tid) L
v 6= null
[Legal-NewThread]
vA
classof(v) declaringType(m)
Fig. 4.
that declares the field. Note that field updates change the heap: the old value of
the field is replaced with the new value. When creating a new object (New), an
unused object reference is chosen from O, is inserted into the heap and all its fields
are initialized to the default value null. New objects are initially only accessible to
their creator and therefore the reference is added to the creating threads access
set. A thread may share (Share) an unshared object in its access set. By doing
so, it removes the object from its access set and adds it to the global shared set
S. Shared objects may be locked (Synchronized) provided they are not locked
yet. As noted in Section 2.1.1.1, we consider lock re-entry to be illegal. Our
method effect framing approach, described in Section 2.2, relies on this. For the
duration of the synchronized block, the lock map L marks the current thread as
holder of the lock. The object is also added to the access set. When the end
of the synchronized block is reached (Unlock), the object must still be in the
threads access set. At this time, the object is removed from the access set and its
corresponding lock is released. Invoking a method m (Call) within a thread tid
results in the addition of a new activation record to tids call stack. The activation
record contains the body of m where all free variables (this and parameters) are
replaced with the corresponding argument values. In the callers activation record,
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
12
[If]
v1 6= v2 s0 = s2
(H, L, S, T / (tid, A, (if (v1 = v2 ) { s1 } else { s2 } s) F)) (H, L, S, T / (tid, A, (s0 s) F))
v 6= null
[Assert]
classof(v)
[Read] (H, L, S, T / (tid, A, (x := v.f ; s) F)) (H, L, S, T / (tid, A, (s[H(v, f )/x]) F))
[Write] (H, L, S, T / (tid, A, (v1 .f := v2 ; s) F)) (H[(v1 , f ) 7 v2 ], L, S, T / (tid, A, (s) F))
o 6 dom(H)
[New]
classof(o) = C
[Share] (H, L, S, T / (tid, A, (share v; s) F)) (H, L, S {v}, T / (tid, A \ {v}, (s) F))
v 6 dom(L)
[Synchronized]
[Unlock] (H, L, S, T / (tid, A, (unlock o; s) F)) (H, L \ {(o, tid)}, S, T / (tid, A \ {o}, (s) F))
s0 = mbody(v.m(v))
[Call]
[Return]
[NewThread]
tid0 6= tid
Fig. 5.
the method invocation is replaced with a receive statement, which keeps a record
of the calls pre-state and arguments. The operands of the receive statement are
not used by the dynamic semantics; they are used only in the soundness proof in
Section 2.2.5. When a method call returns (Return), the top activation record
is popped and the return value is substituted into the callers activation record.
A new thread (NewThread) is started by performing a start o.m(); operation.
Accessibility of object o is transferred from the original thread to the new one. The
new thread consists of a single activation record containing the body of method m
where this is replaced by o.
Program execution starts in an initial state.
Definition 3. In a programs initial state, there is only a single thread. It has
an empty access set and it executes the main routine. Moreover, the heap, the lock
map, and the shared set are all empty.
initial((, , , {(tid, , program main)}))
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
13
14
are allocated.
o dom(H), f dom(H(o)) H(o)(f ) dom(H) {null}
Definition 9. A shared set is well-formed with respect to a heap (H ` S : ok) if
shared objects are allocated.
S dom(H)
Definition 10. A lock map is well-formed with respect to a heap and shared set
(H, S ` L : ok) if locked objects are shared.
dom(L) S
Definition 11. A program state
= (H, L, S, T)
is well-formed (wf()) if the following conditions hold:
The access sets and the free set partition the heap.
]
{A}) ] {S \ dom(L)} dom(H)
(
( ,A, )T
The heap, the shared set, and the lock map are well-formed.
` H : ok H ` S : ok H, S ` L : ok
The continuations in each threads call stack contain only references to allocated
objects and do not contain any free variables.
( , , F) T s F
(objectRefs(s) dom(H) free(s) = )
Notice that well-formedness of a program state implies that access sets are disjoint
and that accessible objects are allocated.
Theorem 1. In a legal program, the small step relation preserves well-formedness.
program legal (1 , 2 (wf(1 ) 1 2 ) wf(2 ))
Proof. By case analysis on the step from 1 to 2 . We consider cases Share,
Synchronized, and Unlock.
Case Share. By legality, we have that the object being shared is in the access
set but not in the shared set. The step adds it to the shared set (and therefore
the free set since unshared objects are not locked) and removes it from the access
set. It follows that the partition is maintained.
Case Synchronized. Assume that the object being locked is o. In 1 , o is
shared and not locked, and therefore it is part of 1 s free set. Since in a wellformed state the free set and the access sets partition the heap, o is not in any
threads access set. Adding o to a single access set and removing it from the free
set (by adding an entry for o to the lock map) maintains the proper partitioning
of the heap. Because locking an object modifies neither the heap nor the shared set
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
15
and both are well-formed in the pre-state, they are well-formed in the post-state.
Adding an entry for o (a shared object) to 1 s well-formed lock set, preserves
the fact that the lock set only contains shared objects. Finally, the continuations
in 2 contain neither free variables nor unallocated objects, because 1 does not
contain any and because locking did not introduce any.
Case Unlock. By legality, the object being unlocked is in the access set and is
locked by the current thread. The step removes it from the lock map (thus adding
it to the free set, since locked objects are shared) and from the threads access set.
It follows that the partition is maintained.
16
and the other is a Read or Write of o.f and the steps are not ordered by the
happens-before relation.
The following lemma states that in legal programs, no ordering constraints exist
on execution steps beyond those imposed by the synchronization constructs (i.e.,
thread creation and synchronized blocks).
Lemma 1. In an execution of a legal program , if two consecutive steps are not
ordered by happens-before, then swapping them results again in an execution of .
Proof. By case analysis on the steps. We detail a few cases.
The steps are not accesses of the same field, since this would mean access sets
are not disjoint, and by Theorem 2 we have that program states are well-formed.
A New step can be moved to the right. The other step does not access the newly
created object since the New step does not modify the thread state of the other
thread and well-formedness of the latter implies it does not mention unallocated
objects.
2.1.5 Non-interference. In this subsection, we introduce the notion of a noninterfering state change and we prove that in executions of legal programs, with
respect to the access set of one thread, steps of other threads are non-interfering
state changes.
Definition 15. Two states are related by a non-interfering state change with
respect to a given access set if
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
17
all objects that are allocated in the first state are also allocated in the second state,
all objects that are shared in the first state are also shared in the second state,
and
if an object is in the access set, then its state in the heap is unchanged and if
additionally the object is unshared in the first state, then it is unshared in the
second state.
Formally:
A
(H, S) ; (H0 , S0 )
m
dom(H) dom(H0 ) S S0 H0 |A = H|A S0 A = S A
A
Note: the non-interfering state change relation ; for a given access set A relates
two heap-shared set pairs rather than full program states. This allows us to re-use
this relation in the context of thread-relevant states (see Section 2.2).
The following theorem states a key property of the programming model. Note:
tid
we write 0 to denote that program states and 0 are related by an execution
step performed by thread tid.
Theorem 4 Thread Isolation. In an execution of a legal program, with respect to the access set of one thread, a sequence of steps of other threads constitutes
a non-interfering state change.
tid
tid
(H0 , S0 ) ; (Hn , Sn )
Proof. Since the program is legal, by Theorem 2 we have that all program states
reached are legal and well-formed. We prove the theorem by induction on n. The
base case n = 0 is trivial. Assume n > 0. By induction we have
A
Static verification
The previous section proved that legal programs are data-race-free. However, legality of a program is not a modular notion. Furthermore, the issue remains of
how to verify legality of a given program. In this section, we define the notion
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
18
of valid programs, which is a condition that is modular and that is suitable for
submission to an automatic theorem prover, and we show that valid programs are
legal. It follows that valid programs are data-race-free and that they perform no
null dereferences or ill-typed operations.
The validity notion is based on provability of verification conditions in the verification logic, i.e., the logic used to interpret the verification conditions.
Before we define and prove the validity notion, we establish the verification logic
and we discuss the modular verification approach.
2.2.1 Verification logic. We target multi-sorted first-order predicate logic with
equality. That is, a term t is a logical variable y Y or a function application
f (t1 , . . . , tn ) where f is a function symbol from the signature with arity n. A
formula is an equality t1 = t2 , an inequality t1 6= t2 , a literal true or false, an
atom P (t1 , . . . , tn ) where P is a predicate symbol from the signature with arity n,
a propositional formula using the connectives , , , and , or a quantification
(y ).
We use the signature shown in Figure 6, for a given program. Note:
All sorts are countably infinite.
We leave the sorts of quantifications, function symbols, and predicate symbols
implicit when they are clear from the context.
The widening and narrowing conversions are inserted implicitly to convert between program values and object references. That is, where below we write a term
t of sort ref in a location where a term of sort value is expected, t should be read
as asvalue(t), and vice versa. Also, some of the function symbols are overloaded.
Specifically, implicitly there is a separate emptyset symbol for each set or function
sort, and there are separate apply, update, and dom symbols for each function sort.
Additionally, the signature contains a nullary function symbol for each class or
interface declared by the program, and a nullary function symbol f for each field
f declared by the program.
Note: we also use the following abbreviations:
Abbreviation
t1 (t2 , t3 )
t1 [(t2 , t3 ) 7 t4 ]
t1 t2
t2 |t1 = t3 |t1
t1 t2
Meaning
t1 (t2 )(t3 )
t1 [t2 7 (t1 (t2 )[t3 7 t4 ])]
(y y t1 y t2 )
(y y t1 t2 (y) = t3 (y))
t1 t2 t1 = t2
19
Notes
Object references
Program values (object reference or null)
Finite sets of object references
Field names
Class names
Interface names
Object states, i.e. finite partial functions
from field names to program values
Heaps, i.e. finite partial functions
from object references to object states
Sorts
set
interface class
Sorts
set or (, ) func
set set
ref set value ref set
set set
set set set
set set set
(, ) func
(, ) func
(, ) func
(, ) func set
value
ref class
ref value
value ref
Fig. 6.
Syntax
t1 t2
t1 t2
Notes
subtype relation
Syntax
t1 {t2 }
t1 {t2 }null
t1 \ {t2 }
t1 t2
t1 t2 or t1 \ t2
t1 (t2 )
Notes
empty set, empty function
t1 [t2 7 t3 ]
function update
dom(t)
null
classof(t)
t
t
function domain
class of an object
implicit widening
implicit narrowing
20
A continuation, as it appears in a program text, may contain free program variables. Consequently, the corresponding continuation verification condition contains
these program variables as free logical variables.
Definition 18. A program variable z Var is either this, result, or a method
parameter or local variable x X .
Var = {this, result} X
A continuation verification condition is a state predicate. A state predicate may
be a two-state predicate or a one-state predicate. A two-state predicate may refer
to the current thread-relevant state (using free variables H, Lt , S, and A) and to the
old
, and Aold ). A one-state predicate may
old state (using free variables Hold , Lold
t , S
refer only to the current state (using free variables H, Lt , S, and A). Since method
postconditions are two-state predicates, so are continuation verification conditions.
Definition 19. A two-state predicate (or state predicate for short) Q is a formula of the verification logic, whose free logical variables are the current and old
thread-relevant state variables and program variables:
old
free(Q) {H, Lt , S, A, Hold , Lold
, Aold } Var
t ,S
21
22
23
only for locked and accessible objects. When invoking a method (VC-Call), the
target should not be null, the callees precondition should hold and when returning (VC-Receive) the postcondition should hold. When a method call returns,
we make some assumptions about the post-state. First of all, as per the method
framing approach, we may assume that the post-state is related to the pre-state
by a non-interfering state change with respect to the pre-state access set minus the
callees required access set. Secondly, we may assume the post-state is well-formed
and satisfies the callees postcondition. Finally, we may assume the return value is
allocated. Starting a new thread via start (VC-NewThread) requires the target
object to be accessible and non-null. Accessibility of the target object is transferred
from the current thread to the new thread.
An important property of continuation verification conditions is that they are
local.
Theorem 5. If a state predicate Q is local, then the verification condition of a
continuation s with respect to Q is local as well.
local(Q) local(vc(s, Q))
Proof. By induction on s.
Case s = synchronized (v) { s00 } s0 .
(1 ) We may assume that s is valid (i.e., vc(s, Q) holds) in a state (H, Lt , S, A).
(2 ) It follows by VC-Synchronized that the continuation s00 unlock v; s0 of
A
(3 ) We need to prove that s is valid in any state (H00 , Lt , S00 , A) where (H, S) ;
(H00 , S00 ).
(4 ) This requires that we prove that the continuation s00 unlock v; s0 of s is
A
valid in any state (H000 , Lt {v}, S000 , A {v}) where (H00 , S00 ) ; (H000 , S000 ).
(5 ) It is easy to see that the non-interfering state change relation is transitive;
A
therefore, from the assumptions in points 3 and 4 we have (H, S) ; (H000 , S000 ).
By instantiating the rule in point 2, we obtain the goal in point 4.
Case receive is analogous to case synchronized.
The other cases are easy.
24
[VC-If]
vc(assert v instanceof ; s, Q)
(v 6= null classof(v) ) vc(s, Q)
[VC-Assert]
vc(x := v.f ; s, Q)
[VC-Read]
v 6= null classof(v) = declaringClass(f ) v A vc(s, Q)[H(v, f )/x]
vc(v1 .f := v2 ; s, Q)
[VC-Write]
v1 6= null classof(v1 ) = declaringClass(f ) v1 A (v2 = null v2 S)
vc(s, Q)[H[(v1 , f ) 7 v2 ]/H]
vc(x := new C; s, Q)
[VC-New]
o o 6 dom(H) classof(o) = C
vc(s, Q)[o/x, H[o 7 [f1 7 null] [fn 7 null]]/H, (A {o})/A]
where fields(C) = {f1 , ..., fn }
vc(share v; s, Q)
v 6= null v A v
/ S vc(s, Q)[(A \ {v})/A, (S {v})/S]
vc(synchronized (v) { s0 } s, Q)
v 6= null v S v 6 Lt
(H0 , S0
[VC-Share]
[VC-Synchronized]
[VC-Unlock]
[VC-Call]
vc(start v.m(); s, Q)
[VC-NewThread]
v 6= null classof(v) declaringType(m) v A vc(s, Q)[(A \ {v})/A]
vc(return v; , Q)
Q[v/result]
Fig. 7.
[VC-Return]
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
25
[VC-Receive]
((H, S) caller
; (H0 , S0 ) (` (H0 , Lt , S0 , A0 ) : ok)
0
A Acaller = A Acaller
Q0 [H0 /H, S0 /S, A0 /A, Lpre
t /Lt , vr /result,
old pre
Hpre /Hold , Lpre
/Sold , Apre /Aold ]
t /Lt , S
0
(vr = null vr dom(H ))
old
, A/Aold ]
vc(s, Q)[o/this, v/x, H/Hold , Lt /Lold
t , S/S
the main routine s is valid:
` vc(s, true)[/H, /Lt , /S, /A]
The example of Figure 3 is a valid program.
2.2.5 Soundness. In this subsection we define a notion of valid program state
and we prove that valid states are legal states. We then prove that program states
reached by valid programs are valid, by proving that the initial state is valid and
that execution steps preserve validity. It follows that valid programs are legal
programs and therefore they are data-race-free.
A program state is valid if each thread state is consistent and each activation
record is valid. The latter means that the continuation verification condition of
the activation records continuation holds with respect to the activation records
postcondition. An activation records validity is independent of actions performed
by other activation records. We prove this using the notion of activation record
access sets. We prove that an activation records validity depends only on the
state of the objects in its access set, and actions of other activation records are
non-interfering with respect to this access set.
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
26
27
A callers pre-state lockset is equal to the set of unlock statements in the calls
continuation plus transitive caller continuations
(i {2, . . . , n} Li = {o | (j {i, . . . , n} sj = unlock o; )})
Each non-top activation records required access set is included in its pre-state
activation record access set
Each non-top activation records pre-state access set includes the access sets of
transitive caller activation records
The threads access set includes the non-top activation records access sets
n=1
(Areq (call n , n ) An
(i {2, . . . , n 1}
Ai+1 Areq (call i+1 , i+1 ) Ai
Areq (call i , i ) Ai (Ai+1 Areq (call i+1 , i+1 )))
A2 Areq (call 2 , 2 ) A)
Notice that if a thread state is consistent, then its activation records access sets
partition the threads access set.
Definition 24. An activation records postcondition Qar(i) (t) is the postcondition of the call stored in the receive statement in the callers continuation, or true
if the activation record has no caller.
Formally:
t = (tid, A, (s1 ) (x2 := receive [2 ] call 2 ; s2 )
. . . (xn := receive [n ] call n ; sn ) )
post(call i+1 , i+1 ) if i < n
i {1, . . . , n} Qar(i) (t) =
true
if i = n
Definition 25. An activation record is valid if the verification condition of its
continuation with respect to its postcondition holds under the current heap, lock set,
and shared set, and under the activation records access set. Formally:
t = (tid, A, (s1 ) . . . (sn ) )
(i {1, . . . , n}
H, L, S ` validar(i) (t)
I, H, L1 (tid), S, Aar(i) (t) vc(si , Qar(i) (t)))
Definition 26. A program state is valid, written valid(H, L, S, T), if all of the
following hold:
it is well-formed
wf(H, L, S, T)
for each thread state t T where t = (tid, A, r1 rn ), all of the following hold:
it is well-formed
` (H, L1 (tid), S, A) : ok
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
28
it is consistent
L1 (tid) ` consistent(t)
each activation record is valid
i {1, . . . , n} H, L, S ` validar(i) (t)
Theorem 6. A valid program state is a legal program state.
Proof. One can easily prove that each thread is in a legal state by performing a
case analysis on the first statement of the continuation of the top activation record
and for each case using the validity of the top activation record.
Theorem 7. In a valid program , the small step relation preserves validity.
1 , 2 (valid(1 ) 1 2 ) valid(2 )
Proof. By case analysis on the step. Note that preservation of well-formedness
is given by Theorem 1. We refer to the thread that performs the step as the current
thread and the top activation record of the current thread as the current activation
record. For each step rule, we have to prove that in 2 , thread states are well-formed
and consistent and activation records are valid.
Each step changes the current activation records continuation. We only note
other changes. Also, we only detail the argument for preservation of validity of the
current activation record if it does not follow easily from the verification condition.
Case If, Assert. The step changes only the continuation of the current activation record. Therefore, thread state consistency and validity of the other activation records is preserved trivially.
Case Write. The step changes the heap at some location o.f , and the current
activation records continuation. Thread state consistency depends on neither
so it is preserved trivially. By VC-Write, we have that o is in the current
activation records access set. Well-formedness of the thread-relevant state is
preserved since the value written into the field is either null or a shared object.
Since activation record access sets are disjoint and, as a result of the locality
of continuation verification conditions (Theorem 5), activation record validity
depends only on heap locations in the activation records access set, validity of
other activation records is preserved trivially.
Case Share. The step changes the shared set and the current threads access
set. Since the object being shared was in the current activation records access
set, it is not in the access set of any other activation record of the current thread
so the access set still contains those. This establishes thread state consistency.
The validity of an activation record is preserved by sharing an object that is not
in its access set, and by removing from the threads access set an object that is
not in the activation records access set, so the validity of non-current activation
records is preserved.
Case Call. The step changes the current activation records continuation and
adds a new activation record. Since the precondition holds, its required access
set is contained in the callers pre-state access set. Therefore, thread state consistency is preserved. Validity of existing non-current activation records is preserved trivially. Since the program is valid, the method being called is valid, and
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
29
the methods body is valid in any state that satisfies the precondition. Note that
the new activation records access set is equal to the preconditions required access
set.
Case Return. The step replaces the caller and callee activation records with
an activation record containing the continuation of the call. Validity of the caller
activation record implies that the calls continuation is valid in any thread state
that a) differs from the current state only as allowed by the methods frame condition and b) satisfies the postcondition. Validity of the callee implies that the
postcondition holds in state 1 . Therefore the calls continuation is valid in state
1 and, therefore, in state 2 .
Case NewThread. The step adds a new thread with a single activation record,
and removes the target object from the creating threads access set. Thread state
consistency of the new thread is trivial. The new activation record is valid since
the method it executes is valid and its precondition, which by well-formedness of
the program must be Lt = this A, is satisfied.
Case Read. The step changes only the current activation records continuation.
Validity follows trivially from VC-Read.
Case New. The step adds an object to the heap domain and the threads access
set. Since by well-formedness access sets contain only allocated objects, access
sets remain disjoint.
Case Synchronized. The step adds an object to the lock set and the access set.
Since by well-formedness the free set is disjoint from access sets and the object
was in the free set, access sets remain disjoint.
Case Unlock. The step removes an object from the threads access set. Since
it was in the current activation records access set, no other activation records
are affected.
This concludes the proof.
Theorem 8. A valid program is a legal program.
Proof. The initial state of a valid program is a valid state. It follows, by Theorem 7, that all reachable states are valid. Therefore, by Theorem 6, all reachable
states are legal. Therefore, the program is legal.
Theorem 9 Main Theorem. Valid programs are data-race-free.
Proof. By combining Theorem 8 and Theorem 3.
3.
30
3.1
Programming model
To prevent race conditions that break the consistency of multi-object data structures, we integrate the Spec# methodologys object invariant and ownership system
[Barnett et al. 2004] into our approach. The model supports objects that use other
objects to help represent their state, and object invariants that express consistency
constraints on such multi-object structures.
The programming model requires the programmer to designate a subset of each
classs fields as the classs rep fields. The objects pointed to by an object os non-null
rep fields in a given program state are called os rep objects. An objects rep objects
may have rep objects themselves, and so on; we refer to all of these as the objects
transitive rep objects. The fields of an object, along with those of its transitive rep
objects, are considered in our approach to constitute the entire representation of
the state of the object; hence the name. As will be explained later, a shared object
os lock protects both o and its transitive rep objects. (Formally, we have o repH p
if a field f exists such that (o, f ) dom(H) and H(o, f ) 6= null and H(o, f ) = p and
f is a rep field. We use repH to denote the reflexive-transitive closure of repH . If R
is a relation on elements x, then we use R(x) to denote {y | x R y}.)
In addition to a set of rep fields, the programming model requires the programmer
to designate, for each class C, an object invariant, denoted inv(C). inv(C) is a
predicate whose free variables are the heap H, the shared set S, and the target
object this. inv(C) must depend only on the state of this, that is, the fields of this
and of the transitive rep objects of this. Also, it must be preserved by growth of
the shared set. Formally:
H, S, this, H0 , S0 inv(C) H0 |repH (this) = H|repH (this) S S0 inv(C)[H0 /H, S0 /S]
The object invariant for an object o need not hold in each program state. Rather,
the programming model introduces a new global state variable P, called the packed
set, which denotes a set of objects. The object invariant for an object o needs to
hold only in a state where o P.
The programming model requires an object to be in the packed set when a thread
shares the object or unlocks it, i.e. when the object becomes free. It follows that
each free object is in the packed set and its object invariant holds. As a result,
when a thread locks an object, it may assume that the object is in the packed set
and its object invariant holds.
In the programs initial state, the packed set is empty, and newly created objects
are not in the packed set. The programmer may move an object into or out of the
packed set using new pack o; and unpack o; annotations.
To ensure that whenever an object is in the packed set, its object invariant holds,
the programming model imposes the following restrictions:
A thread may assign to an objects fields only when the object is in the threads
access set and the object is not in the packed set. Furthermore, the remaining
restrictions ensure that whenever an object is in the packed set, then so are its
transitive rep objects. As a result, an objects state (defined as the values of its
fields and those of its transitive rep objects) does not change while it is in the
packed set.
A thread is allowed to perform a pack o; operation only when o is in the threads
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
31
access set, its object invariant holds, it is not yet in the packed set, and its rep
objects are in the threads access set and in the packed set. Furthermore, besides
inserting the object into the packed set, the operation removes os rep objects
from the threads access set.
A thread is allowed to perform an unpack o; operation only when o is in the
threads access set and in the packed set. The operation removes o from the
packed set and adds os rep objects to the threads access set.
We say that an object owns its rep objects whenever it is in the packed set. It
follows from the above restrictions that an object has at most one owner.
Note that our approach supports ownership transfer; a rep object can be moved
from one owner to another by first unpacking both owners and then simply updating
the relevant rep fields.
The updated syntax rules are as follows:
class ::= class C implements I { field invariant ; meth }
field ::= rep? f ;
s ::= | packC g; | unpackC g;
vA
repH (v) A
[Pack] (H, L, S, P, T / (tid, A, (packC v; s) F)) (H, L, S, P {v}, T / (tid, A \ repH (v), (s) F))
v 6= null
[Legal-Unpack]
v AP
[Unpack]
(H, L, S, P, T / (tid, A, (unpackC v; s) F)) (H, L, S, P \ {v}, T / (tid, A repH (v), (s) F))
v1 6= null
[Legal-Write]
v1 A \ P
classof(v1 ) = declaringClass(f )
More programs are legal in the programming model of this section (that is, more
programs can be augmented with share, rep, pack, and unpack annotations so
that the annotated program is legal under the programming model) since objects
may be protected against data races by the lock of a transitive owner.
The definition of well-formedness of program states is updated as follows. The
rule saying that the access sets and the free set partition the heap is replaced by
a rule saying that the access sets, the free set, and the rep sets of packed objects
(that is, the sets repH (o) of objects o P) partition the heap.
The definition of non-interfering state change is updated as follows:
Definition 27. Two states are related by a non-interfering state change with
respect to a given access set if
all objects that are allocated in the first state are also allocated in the second state,
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
32
all objects that are shared in the first state are also shared in the second state,
and
if an object is in the access set, then
the values of its fields are unchanged, and
if it is unshared in the pre-state, it is unshared in the post-state, and
if it is packed in the pre-state, it is packed in the post-state.
Formally:
A
(H, S, P) ; (H0 , S0 , P0 )
m
dom(H) dom(H0 ) S S0 H0 |A = H|A S0 A = S A P0 A = P A
Note: the object invariants and the monitor invariant (i.e. the invariant that
says that free objects are packed) are not part of the programming model; they are
part of the modular verification approach.
3.2
Program annotations
if v = null
objrefs(v) {v} if v O
v
if v P(O)
repH (o)
objrefs(H(o, f ))
f repfields(classof(o))
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
33
class Application {
Rectangle windowBounds;
invariant windowBounds S;
void paint()
requires Lt = ;
requires this A P;
ensures this A P;
}
class Rectangle {
rep Point ul, lr ;
invariant ul.x lr .x ul.y lr .y;
void move(int dx , int dy)
requires this A P;
ensures this A P;
{
unpackRectangle this; ul.move(dx , dy);
lr .move(dx , dy); packRectangle this;
}
int getHeight()
requires this A P;
{
int height;
synchronized (windowBounds) {
height := windowBounds.getHeight();
}
...
}
}
class WindowManager {
Rectangle windowBounds;
invariant windowBounds S;
void mouseDragged(int dx , int dy)
requires Lt = ;
requires this A P;
ensures this A P;
ensures this A P;
{
ensures 0 result;
synchronized (windowBounds) {
windowBounds.move(dx , dy);
}
{
unpackRectangle this;
int h := lr .y ul.y;
packRectangle this; return h;
}
}
}
}
Fig. 8. An example illustrating the data race prevention approach, combined with object invariants
and ownership. (In predicates, we abbreviate H(o, f ) as o.f . Also, we abbreviate this.f as f , and
we show variable types and method return types as an aid to the reader. In the example, we use
integer values and operations in the program and in the verification logic. These are not in the
formal development as they pose no difficulties.)
The example also shows how the approach supports ownership transfer. Method
append transfers ownership of the non-sentinel nodes of other to this.
Note: An alternative to using set-valued ghost rep fields is to exploit the transitive
nature of ownership. In the example, this would mean marking fields first and next
as rep. A difficulty with this approach, however, is that modifying the ith node
requires i + 1 unpack and pack operations, to unpack the nodes transitive owners
and the node itself before the modification, and then pack all of these objects in
the reverse order afterwards. This practically imposes the use of recursion, which
may be undesirable, especially for constant-time operations such as method append
in the example.
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
34
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
class Rational {
int p, q;
invariant q 6= 0;
void multiply(int z)
requires this A P;
ensures this A P;
{ unpack this; p := p z; pack this; }
}
class RationalBuffer {
rep Rational element;
void put(Rational x)
requires this S \ Lt x 6= null x A P;
ensures true;
{
synchronized (this) {
if (this.element = null) {
done := true;
unpack this;
this.element := x;
pack this;
} else {
done := false;
}
}
if (done) { put(x); }
}
Rational get()
requires this S \ Lt ;
ensures result 6= null result A P;
{
synchronized (this) {
unpack this;
x := this.element;
this.element := null;
pack this;
}
if (x = null) { x := get(); }
return x;
}
}
35
class Producer {
RationalBuffer buffer ;
invariant buffer 6= null buffer S;
void run()
requires Lt = this A P;
ensures true;
{
r := new Rational;
r.p := 1;
r.q := 1;
pack r;
buffer .put(r);
this.run();
}
}
class Processor {
RationalBuffer buffer1 , buffer2 ;
invariant buffer1 6= null buffer1 S;
invariant buffer2 6= null buffer2 S;
void run()
requires Lt = this A P;
ensures true;
{
r := buffer1 .get();
r.multiply(7);
buffer2 .put(r);
this.run();
}
}
class Consumer {
RationalBuffer buffer ;
invariant buffer 6= null buffer S;
void run()
requires Lt = this A P;
ensures true;
{
r := buffer1 .get();
// Print r.p and r.q (not shown)
this.run();
}
}
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
36
In our approach, once an object is shared, it never reverts to the unshared state.
This has the advantage that, when verifying a method, it may be assumed that if
an object is shared at a given point in time, then it will be shared in all subsequent
program states, regardless of the actions of other threads. Note also that this does
not prevent scenarios where an object is passed from one thread to another and
then accessed without locking. This is illustrated by the example in Figure 10.
In the example, an unshared Rational object is passed from a producer thread to
a processor thread and then on to a consumer thread. Each thread accesses the
object without locking. Each transfer proceeds via a shared RationalBuffer object:
first, the RationalBuffer object is locked and unpacked. Then, a reference to the
Rational object is stored in the RationalBuffer objects element field, which is a
rep field. Then, the RationalBuffer object is packed, which causes the Rational
object to be removed from the threads access set and to become owned by the
RationalBuffer object. Then, the RationalBuffer object is unlocked. When the
receiver thread subsequently locks the RationalBuffer object, it unpacks it, which
causes it to relinquish ownership of the Rational object and causes the latter to
be added to the receiver threads access set. Finally, clearing the element field
prevents the Rational object from again becoming owned by the RationalBuffer
object when the latter is packed again.
Note: The example of Figure 10 shows how invariants can be used to state that
certain fields hold shared objects. This is why the built-in invariant of the previous
section that all fields hold shared objects, is no longer needed.
3.3
Static verification
The definition of program well-formedness is updated as follows. The rule concerning the method contract of a method used in a start statement is replaced with
the following: If a method is used in a start statement, then its precondition must
be exactly Lt = this A P.
The definition of thread-relevant state is updated to include the packed set.
= (H, Lt , S, P, A)
The definition of well-formedness of a thread-relevant state is updated as follows.
The following conjunct is added: if an object is in the packed set, then its object
invariant holds and its rep objects are also in the packed set.
o P inv(C)[o/this] repH (o) P
Also, the conjunct that says that objects pointed to by fields are shared is dropped,
since now object invariants can be used to express this.
New and updated continuation verification condition rules are shown in Figure 11.
The definition of validity of a program state is updated by adding a conjunct
saying that free objects are packed.
S dom(L) P
Theorem 10. In the approach of this section, legal programs are data-race-free,
and valid programs are legal.
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
37
vc(v1 .f := v2 ; s, Q)
[VC-Write]
v1 6= null classof(v1 ) = declaringClass(f ) v1 A \ P vc(s, Q)[H[(v1 , f ) 7 v2 ]/H]
vc(share v; s, Q)
v 6= null v A P v
/ S vc(s, Q)[(A \ {v})/A, (S {v})/S]
vc(synchronized (v) { s0 } s, Q)
v 6= null v S v 6 Lt
(H0 , S0 , P0
[VC-Share]
[VC-Synchronized]
[VC-Unlock]
[VC-Call]
vc(start v.m(); s, Q)
[VC-NewThread]
v 6= null classof(v) declaringType(m) v A P vc(s, Q)[(A \ {v})/A]
old , Pold , Aold )] o.m(v); s, Q)
vc(x := receive [(Hold , Lold
t ,S
H0 , S0 , P0 , A0 , vr
[VC-Receive]
Aold A(P 0 )
((H, S, P)
;
(H0 , S0 , P0 ) ` (H0 , Lt , S0 , P0 , A0 ) : ok
A0 (Aold A(P 0 )) = A (Aold A(P 0 ))
Q0 [H0 /H, S0 /S, P0 /P, A0 /A, Lold
t /Lt , vr /result]
(vr = null vr dom(H0 ))
4.
[VC-Pack]
[VC-Unpack]
LOCK RE-ENTRY
In the programming model preceding sections, lock re-entry is ruled out. That
is, a program that re-enters a lock is considered illegal and invalid. However, it
is not difficult to add support for lock re-entry to the programming model of the
preceding section. We show an approach where a lock re-entry is treated like a
no-op. In this approach, the only modification required to the programming model
is to relax the legality rule and to add a second step rule for synchronized blocks,
and the only modification required to the static verification approach is to add a
case split to the verification condition rule for synchronized blocks. The new or
updated definitions are shown in Figure 12.
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
38
vS
[Synchronized-Reentrant]
v L1 (tid)
[VC-Synchronized]
Program
Lines
of Code
chat
phone
prod-cons
philosophers
344
222
84
64
Lines
Changed
or Added
117
50
24
21
Table I.
5.
Overhead
34%
23%
29%
33%
After
Defaults and
Inference
41
14
0
5
Net Overhead
13%
6%
0%
8%
Annotation overhead
EXPERIENCE
To verify the applicability of our approach to realistic, useful programs, we implemented it in a custom build of the Spec# program verifier [Barnett et al. 2006]
and used it to verify four programs written in C# with annotations inserted in the
form of specially marked comments. The approach that we implemented includes
elements omitted from this article, including deadlock prevention and immutable
objects [Jacobs 2007]. Each program verifies successfully; this guarantees the following:
The program is free from data races and deadlocks.
Object invariants, loop invariants, method preconditions and postconditions, and
assert statements declared by the program hold.
The program is free from null dereferences, array index out of bounds errors, and
typecasting errors.
The program is free from races on platform resources such as network sockets.
This is achieved by enforcing concurrency contracts on the relevant API methods.
Table I shows the annotation overhead of the four programs which we annotated
and verified. Programs chat and phone were derived from the ones used in [Boyapati
et al. 2002].
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
39
6.
DISCUSSION
In this article, we propose a programming model and verification approach for multithreaded object-oriented programs. We focused on designing a simple approach
to multithreading that integrated well with the Boogie approach [Barnett et al.
2004] for object invariants and dynamic ownership, yielding what we believe to be
the first sound program verification approach that supports both multithreading,
dynamic ownership, and object invariants over an object and its transitively owned
objects.
Our approach is not complete. That is, not every Java program that is datarace-free can be annotated such that verification using our approach succeeds. The
incompleteness exists at three levels: the programming model, the verification approach, and the theorem prover.
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
40
6.1
Programming model
Not every program that is data-race-free complies with the programming model, or
can be made to do so by inserting share, rep, pack, and unpack annotations.
A basic limitation is that objects, not fields, are in access sets; therefore, two
threads can never access distinct fields of a given object concurrently. We inherited
this limitation from the Boogie approach. One way to lift it would be to drop the
Boogie approach in favor of an approach based on dynamic frames [Kassios 2006],
which subsume the Boogie approachs support for dynamic ownership and object
invariants. However, the suitability of dynamic frames for automatic verification
has not been shown.
Our approach, as described in this article, does not distinguish between read and
write access. However, it is fairly easy to replace access sets with read sets and
write sets, and this has been implemented in the prototype verifier. Based on read
sets and write sets, it is easy to add support for unrestricted sharing of immutable
objects, and for reader-writer locks.
Programs that use synchronization constructs other than Javas synchronized
blocks may or may not be supported. For example, in Figure 10, a RationalBuffer
object can be used as a binary semaphore, where put and get calls correspond to
V and P operations, respectively. However, programs where volatile fields are used
to protect data structures, for example, are not supported. Still, in some cases
it might be possible to encapsulate an unsupported construct within a class and
then enforce the correct use of the class by annotating the classs methods with
appropriate method contracts and verifying client code using our approach.
6.2
Static verification
Even if a program complies with our programming model, it might not be a valid
program; i.e., it might not be possible to annotate each method with a method
contract and each class with an invariant, so that in the resulting program, each
methods verification condition holds. The main source of incompleteness on this
level is the imprecise modeling of inter-thread interference.
In our verification approach, when verifying a thread, the interference of other
threads is taken into account by generating verification conditions as if on entry to a
synchronized block, an arbitrary new value is assigned to each field of each object
that is not in the access set, with the only restriction being that if an object is in the
packed set, its object invariant holds. This means that any monotonicity properties
preserved by the program are not taken into account. For example, if an integer field
of a shared object is only ever incremented, and never decremented, by threads that
lock the object, it is still not possible to prove, in our approach, that a value read by
a thread from the field is greater than or equal to a value read by that thread in an
earlier synchronized block. It seems possible to lift this source of incompleteness
by extending our approach with support for rely-guarantee conditions. In this
extended approach, a non-interfering state change would be defined as one that,
in addition to the current requirements, satisfies the rely condition. The locality
requirement on method preconditions and postconditions would become weaker
accordingly: a contract may mention state outside the access set, provided it is
preserved by state changes that satisfy the rely condition.
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
41
Note that the ability to insert ghost field declarations and ghost field updates
into the program is also essential for completeness. Indeed, a methods correctness
may depend on the local states of other threads. For example, suppose one thread
initially only increments a shared counter, and then, after it receives a message
through some shared message queue, it only decrements the counter. A method
executing in another thread may depend on the first thread being in its initial
state. However, since neither method contracts nor rely conditions may mention
threads local state (i.e. their call stacks), there is no way to express this, except
by mirroring a threads local state in the global state using ghost fields which are
kept up to date by the thread.
Given rely conditions and ghost fields, we can now show completeness. Take
an arbitrary program that complies with the programming model. Insert a single
static integer-valued ghost field. It will at all times contain a Godel-like encoding
of the entire program state. Initialize it with an encoding of the initial program
state. After each statement in each method, insert a ghost field update that updates
the ghost field to reflect the new program state. As the rely condition, take the
condition that says that the old and new global state corresponds to the program
states encoded in the old and new values of the ghost field, and that the program
state encoded in the new value of the ghost field is reachable from the program
state encoded in the old value of the ghost field through steps by threads other
than the current thread. Remember that reachability is definable in arithmetic. As
a methods precondition, take the condition that says that the pre-state corresponds
to the ghost field value and that it is reachable from the initial state. As a methods
postcondition, take the condition that says that the state corresponds to the ghost
field value and that the post-state is reachable from the pre-state.
6.3
Theorem prover
The third source of incompleteness is the proof step. There are two aspects to this:
there is a theoretical limitation, and additionally, there is a practical limitation. The
theoretical limitation is that not all true statements in arithmetic, and therefore in
our verification logic, are provable from any given axiomatization. Indeed, for any
theory , there is a program that is data-race-free and yet its data-race-freedom is
not provable from . (Consider the program that enumerates all proofs, and, if it
finds a proof of its own data-race-freedom, performs a data race.)
The practical limitation is that even provable statements are often not proved
within a reasonable time bound by automatic theorem provers. After all, even
propositional satisfiability is NP-complete. This is a serious concern for our approach. In Section 5, we report on our initial experience in this respect.
7.
RELATED WORK
The present approach evolved from [Jacobs et al. 2005a], [Jacobs et al. 2006], and
[Jacobs 2007]. It improves upon this prior work by adding a formalization of the
approach with invariants and ownership. (A soundness proof [Jacobs et al. 2005b]
accompanies [Jacobs et al. 2005a] but it does not formalize verification condition
generation, and it does not formalize or prove the method effect framing approach.)
As did the prior work, the present approach builds on and extends the Spec#
programming methodology [Barnett et al. 2004] that enables sound reasoning about
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
42
43
addresses the issue of data structure abstraction and data structure composition,
which is not addressed by the Calvin-R work. (They do address a different type of
abstraction: an atomic action specification in a performs annotation abstracts over
the particular operations performed to implement the atomic action.)
A number of type systems have been proposed that prevent data races in objectoriented programs. For example, Boyapati et al. [Boyapati et al. 2002] parameterize
classes by the protection mechanism that will protect their objects against data
races. The type system supports thread-local objects, objects protected by a lock
(its own lock or its root owners lock), read-only objects, and unique pointers. The
system does not support forms of ownership transfer other than transfer of unique
pointers. For example, it cannot type the program of Figure 9. Also, the type
system does not support object invariants.
We enable sequential reasoning and ensure consistency of aggregate objects by
preventing data races. Some authors propose pursuing a different property, called
atomicity, either through dynamic checking [Flanagan and Freund 2004], by way of
a type system [Flanagan and Qadeer 2003], or using a theorem prover [Rodrguez
et al. 2005]. An atomic method can be reasoned about sequentially. However, we
enable sequential reasoning even for non-atomic methods, by assuming only the
object invariant for a newly acquired object (see Figure 7). Also, in [Flanagan and
Qadeer 2003] the authors claim that data-race-freedom is unnecessary for sequential
reasoning. It is true that some data races are benign, even in the Java and C#
memory models; however, the data races allowed in [Flanagan and Qadeer 2003] are
generally not benign in these memory models; indeed, the authors prove soundness
only for sequentially consistent systems, whereas we prove soundness for the Java
memory model, which is considerably weaker.
Abrah
am-Mumm et al. [Abrah
am-Mumm et al. 2002] propose an assertional
proof system for Javas reentrant monitors. It supports object invariants, but these
can depend only on the fields of this. No claim of modular verification is made.
The rules in our methodology that an object must be valid when it is released,
and that it can be assumed to be valid when it is acquired, are taken from Hoares
work on monitors and monitor invariants [Hoare 1974].
There are also tools that try dynamically to detect violations of safe concurrency.
A notable example is Eraser [Savage et al. 1997]. It finds data races by looking for
locking-discipline violations. The tool has been effective in practice, but does not
come with guarantees about the completeness nor the soundness of the method.
This article focuses on programs that use synchronized blocks for synchronization. Significant research has been done on improving the implementation of
synchronized blocks in virtual machines and/or compilers, so that, while preserving Java semantics, opportunities for parallelism are increased. For example, some
proposals infer, fully automatically or aided by annotations, fine-grained locking
schemes. Others propose applying a form of optimistic concurrency, such as transactional monitors [Welc et al. 2004]. Typically, these schemes require the input
program to be data-race-free; therefore, our approach is equally applicable in these
settings.
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
44
8.
CONCLUSION
We propose a programming model for concurrent programming in Java-like languages, and the design of a set of program annotations that make the use of the
programming model explicit and that enable automated verification of compliance.
Our programming model ensures absence of data races, and provides a sound approach for local reasoning about program behavior. We have prototyped the verifier
as a custom build of the Spec# programming system. Through a case study we
show that the model supports non-trivial, useful programs, and we assess the annotation overhead.
Future work includes extending the programming model to encompass read-write
locks, reducing the annotation overhead, and obtaining further experience.
ACKNOWLEDGMENTS
The authors would like to thank the anonymous reviewers for their valuable comments.
REFERENCES
m-Mumm, E., de Boer, F. S., de Roever, W.-P., and Steffen, M. 2002. VerificaAbrah
a
tion for Javas reentrant multithreading concept. In Proc. Foundations of Software Science
and Computation Structures (FoSSaCS), M. Nielsen and U. Engberg, Eds. Lecture Notes in
Computer Science, vol. 2303. Springer, 520.
Barnett, M., Chang, B.-Y. E., DeLine, R., Jacobs, B., and Leino, K. R. M. 2006. Boogie: A
modular reusable verifier for object-oriented programs. In Proc. Formal Methods for Components and Objects (FMCO), F. S. de Boer, M. M. Bonsangue, S. Graf, and W.-P. de Roever,
Eds. Lecture Notes in Computer Science, vol. 4111. Springer, 364387.
hndrich, M., Leino, K. R. M., and Schulte, W. 2004. Verification
Barnett, M., DeLine, R., Fa
of object-oriented programs with invariants. Journal of Object Technology 3, 6, 2756.
Barnett, M., Leino, K. R. M., and Schulte, W. 2004. The Spec# programming system: An
overview. In Proc. Construction and Analysis of Safe, Secure, and Interoperable Smart Devices
(CASSIS), G. Barthe, L. Burdy, M. Huisman, J.-L. Lanet, and T. Muntean, Eds. Lecture Notes
in Computer Science, vol. 3362. Springer, 4969.
Boyapati, C., Lee, R., and Rinard, M. 2002. Ownership types for safe programming: Preventing
data races and deadlocks. In Proc. Object-Oriented Programming Systems, Languages and
Applications (OOPSLA), S. Matsuoka, Ed. SIGPLAN Notices 37, 11, 211230.
Detlefs, D. L., Leino, K. R. M., Nelson, G., and Saxe, J. B. 1998. Extended static checking.
Research Report 159, Compaq Systems Research Center.
Flanagan, C. and Freund, S. N. 2004. Atomizer: A dynamic atomicity checker for multithreaded
programs. In Proc. Principles of Programming Languages (POPL), X. Leroy, Ed. ACM, 256
267.
Flanagan, C., Freund, S. N., Qadeer, S., and Seshia, S. A. 2005. Modular verification of
multithreaded programs. Theoretical Computer Science 338, 1-3, 153183.
Flanagan, C., Leino, K. R. M., Lillibridge, M., Nelson, G., Saxe, J. B., and Stata, R. 2002.
Extended static checking for Java. In Proc. Programming Language Design and Implementation
(PLDI), L. J. Hendren, Ed. SIGPLAN Notices 37, 5, 234245.
Flanagan, C. and Qadeer, S. 2003. A type and effect system for atomicity. In Proc. Programming Language Design and Implementation (PLDI), S. Amarasinghe, Ed. ACM, 338349.
Freund, S. N. and Qadeer, S. 2004. Checking concise specifications for multithreaded software.
Journal of Object Technology 3, 6, 81101.
Gosling, J., Joy, B., Steele, G., and Bracha, G. 2005. The Java Language Specification (3rd
Edition). Prentice Hall.
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
C
I
m
f
x
s
g
v
Lt
Page
4
4
6
6
6
6
6
6
6
6
6
6
6
6
6
6
6
6
6
6
6
7
T
fields(C)
CI
x, v, s
classof(o)
mbody(o.m(v))
7
7
9
10
10
10
10
10
declaringClass(f )
declaringType(m)
10
10
objectRefs()
free()
f [a 7 b]
s[v/x]
[t/x]
ht
S , S 0
H
L
S
T
F
H, L, S ` t : legal
1 2
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
1 2
s1 s2
11
11
11
T/t
initial()
legal()
program wf
12
13
13
13
program legal
x (x)
R
S \ S0
S S0
13
13
13
13
13
45
Meaning
an object reference (Note: null is not an object reference)
a thread identifier
the set of class names
the set of interface names
the set of method names
the set of field names
the set of variable names
the set of logical formulae
the set of object references
the set of thread-relevant states
a class name
an interface name
a method name
a field name
a variable name
a statement
a logical formula
a thread-relevant state
a program
an expression (i.e., this or a variable name or or a literal)
a value (i.e., null or an object reference)
a lockset (i.e., the set of objects whose lock is held by the current
thread)
an access set
the empty set or the empty function
the set of thread identifiers
the set of fields declared by class C
class C mentions interface I in its implements clause
a sequence of variables, values, statements
the name of the class of an object o
the body of method m declared by classof(o), with o substituted for
this and v for the methods parameters
the name of the class that declares field f
the name of the interface that declares method m, or the name of the
class that declares method m if no interface declares a method m
the set of object references in syntactic entity
the set of free variables in syntactic entity
function update
substitution of a value for a variable in a statement list
substitution of a term for a variable in a formula
the empty sequence
the sequence with head h and tail t
a program state
the set of finite partial functions from set S to set S 0
a heap
a lock map
a shared set
a thread set
a call stack (i.e., a sequence of activation records)
thread state t is legal in the given context
an execution step is possible from program state 1 to program state
2
a class or interface name
a shorthand for 1 2 1 = 2
the concatenation (i.e., sequential composition) of statement lists s1
and s2
a shorthand for T {t}
program state is an initial program state
program state is a legal program state
the program is well-formed (Note: Here and throughout, the program
is implicit)
the program is legal
statement (x) holds for all x
the reflexive and transitive closure of relation R
set difference
multiset difference
Table II.
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)
46
Notation
ST
H ` S : ok
` H : ok
H, S ` L : ok
wf()
S ] S0
Page
13
13
14
14
14
14
Meaning
the multiset of sets S partitions set T
shared set S is well-formed in the given context
heap H is well-formed
lock map L is well-formed in the given context
program state is well-formed
multiset union
(H, S) ; (H0 , S0 )
17
f |S
t
I
` (H, Lt , S, A) : ok
I, H, Lt , S, A, V Q
17
18
18
19
20
20
local(Q)
A()
vc(s, Q)
21
21
22
pre(o.m(v))
post(o.m(v))
`
post(o.m(v), )
H , L , S , A
25
25
25
26
26
call
Areq (call, )
Aar(i) (t)
26
26
26
Lt ` consistent(t)
Qar(i) (t)
26
27
H, L, S ` validar(i) (t)
valid()
27
27
a state with heap H and shared set S is related to a state with heap
H0 and shared set S0 by a non-interfering state change with respect
to access set A
function restriction
logical term
the intended interpretation of the verification logic
an axiomatization of I
thread-relevant state (H, Lt , S, A) is well-formed
the truth of a state predicate Q under interpretation I, in threadrelevant state Lt , S, A, and under program variable valuation V
state predicate Q is local
the required access set of a formula
the continuation verification condition of statement list s under postcondition Q
the precondition of call o.m(v)
the postcondition of call o.m(v) (with unbound pre-state)
formula is provable from theory
the postcondition of call o.m(v) with respect to pre-state
the heap, lockset, shared set, resp. access set of thread-relevant state
o repH p
inv(C)
P
objrefs(v)
P(S)
30
30
30
32
32
47
Qadeer, S., Rajamani, S. K., and Rehof, J. 2004. Summarizing procedures in concurrent
programs. In Proc. Principles of Programming Languages (POPL), X. Leroy, Ed. ACM, 245
255.
Rodrguez, E., Dwyer, M., Flanagan, C., Hatcliff, J., Leavens, G. T., and Robby. 2005.
Extending sequential specification techniques for modular specification and verification of
multi-threaded programs. In Proc. European Conference on Object-Oriented Programming
(ECOOP), A. P. Black, Ed. Lecture Notes in Computer Science, vol. 3586. Springer, 551576.
Savage, S., Burrows, M., Nelson, G., Sobalvarro, P., and Anderson, T. E. 1997. Eraser:
A dynamic data race detector for multi-threaded programs. ACM Transactions on Computer
Systems 15, 4, 391411.
Welc, A., Jagannathan, S., and Hosking, A. L. 2004. Transactional monitors for concurrent
objects. In Proc. European Conference on Object-Oriented Programming (ECOOP), M. Odersky, Ed. Lecture Notes in Computer Science, vol. 3086. Springer, 519542.
ACM Trans. on Programming Languages and Systems, Vol. 31, No. 1, December 2008. (PREPRINT)