50.530: software engineering sun jun sutd. week 13: rely-guarantee reasoning
TRANSCRIPT
Background: Race Conditions
Due to the race, it is in general not safe to have multiple thread modifying a memory location at the same time.
0
1
Thread10
1
Thread2
count++ count++
0
1
Thread3
count++
0
1
Thread4
count++ ……
Background: Locking
0
1
Thread1
0
1
count++ count++
0
1
count++
0
1
count++ ……
0 0Thread2
0Thread3
0Thread4
lock(e) lock(e) lock(e) lock(e)
1 1 1 1
……unlock(e) unlock(e) unlock(e) unlock(e)
The scheduler has to maintain many locks and becomes the performance bottleneck.
Disadvantage of Locking
• The ratio of scheduling overhead to useful work can be quite high when the lock is frequently contended – due to context switch and scheduling delays.
• A thread with the lock may be delayed (due to a page fault, scheduling delay, etc.).
• Locking is simply a heavyweight mechanism for simple operations like count++
Hardware Support for Concurrency
• Processors designed for multiprocessor operation provide special instructions for managing concurrent access to shared variables, for example:– compare-and-swap– load-linked/store-conditional
• OSs and JVMs use these instructions to implement locks and concurrent data structures
Compare and Swap
• CAS has three operands – a memory location V, – the expected old value A, – and the new value B.
• CAS updates V to the new value B, but only if the value in V matches the expected old value A; otherwise, it does nothing. In either case, it returns the value currently in V.
public class SimulatedCAS { private int value;
synchronized int get() { return value; }
synchronized int compareAndSwap(int expectedValue, int newValue) { int oldValue = value; if (oldValue == expectedValue) value = newValue; return oldValue; }}
A Non-blocking Counterpublic class CasCounter { private SimulatedCAS value;
public int getValue() { return value.get(); }
public int increment() { int v; do { v = value.get(); } while (v != value.compareAndSwap(v, v + 1)); return v + 1; }}
Is it more efficient than a lock-based counter? Any potential problem?
CAS is used to implement AtomicXxx in Java
CAS in Java
• CAS is supported in atomic variable classes (12 in java.util.concurrent.atomic), which are used, to implement most of the classes in java.util.concurrent– AtomicInteger, AtomicBoolean, AtomicReference,
etc.
Non-blocking Algorithms
• An algorithm is called non-blocking if failure or suspension of any thread cannot cause failure or suspension of another thread;
• Non-blocking algorithms are immune to deadlock (though, in unlikely scenarios, may exhibit livelock or starvation)
• Non-blocking algorithms are known for – Stacks (Treiber’s), queues, hash tables, etc.
Non-blocking Stack
• Considerably more complicated than their lock-based equivalent
• The key is figuring out how to limit the scope of atomic changes to a single variable
Click here for a sample program: ConcurrentStack.java
VERIFYING PROPERTIES OF PARALLEL PROGRAMS: AN AXIOMATIC APPROACH
Owichi et al. Communication of the ACM, 1976
• Is the following sound?
• Yes, if P1 and P2 have no shared variable.
An Unsound Hoare Rule
{ pre1 } P1 { post1 }, { pre2 } P2 { post2} { pre1 && pre2 } P1 || P2 { post1 && post2 }
What if there are shared variables?
An Example
• The following Hoare triples hold:{ y = 1 } x := 0 { y = 1 } { true } y := 2 { true }
• Is the following valid (assuming assignment is atomic)?
{ y = 1 true } x := 0 || y := 2 { y = 1 true }⋀ ⋀
P2 y:=2 is interfering the post-condition of P1!
Another Example
• The following Hoare triples hold: { z = 0 } x := z; y := x { y = 0 }
{ true } x := 2 { true }• The following does not. { z = 0} (x := z; y := x) || x := 2 { y = 0}
P2 x:=2 is interfering the prove on P1!
Interference
• Interference is somehow the problem? What does interfere really means? – Does bal := bal + 1 interfere with { bal > 1000 } ?
• Definition 1: If a program never modifies my variable, it is not interfering! Too restrictive.
• Definition 2: { P } C { P } always holds, i.e., the assertion P is not invalidated by the execution of C. Alternative intuition: if a thread went to a state where P holds, it is not a problem if another thread executes C. – Example: { bal > 1000 } bal := bal + 1 { bal > 1000 }.
Proof Outline
{ T }if (bal > 1000) {{ bal > 1000 } credit := 1;{ credit = 1 => bal > 1000 }}else {{ true } credit := 0;{ credit = 1 => bal > 1000 }}{ credit = 1 => bal > 1000 }
{ T } if (bal > 1000) { credit := 1;else { credit := 0;}{ credit=1 => bal>1000 }
A proof outline puts assertions before and after every atomic statement in the program; and for every statement, the assertion before and after and itself form a Hoare triple.
Interference Freedom
• Given the proof outline O1 of {pre1} P1 {post1} and O2 of {pre2} P2 {post2}, O2 is not interfering with O1 if for every Hoare triple in O2, say {p}c{q}, and for every assertion A in O1, the following is true:
{ p A } c { A }⋀i.e., A remains true for any statement in P2.
• O1 and O2 are interference-free if they do not interfere with each other.
Owicki-Gries Proof Rule
• Step 1: Annotate an assertion to every control point, and show that every Hoare triple holds.
• Step 2: Prove interference freedom: every assertion used in the local proof is shown not invalidated by the execution of the other thread.
{ pre1 } P1 { post1 }, { pre2 } P2 { post2} { pre1 && pre2 } P1 || P2 { post1 && post2 }
if the proofs of { pre1 } P1 { post1 } and { pre2 } P2 { post2 } are interference-free.
Owicki-Gries Proof Method
Prove { x = 0 } x = x+1; || x = x+2; { x = 3 }.
Proof: Let P1 = (x = 0 x = 2), Q1 = (x = 1 x = 3), P2 = (x = 0 ⋁ ⋁ ⋁x = 1), Q2 = (x = 2 x = 3).⋁ Step 0: we show (x=0 => P1 P2) and (Q1 Q2=> x=3).⋀ ⋀Step 1: we show that the following are true.
{ P1 } x := x + 1 { Q1 }{ P2 } x := x + 2 { Q2 }
Assume that each statement is atomic for now.
Owicki-Gries Proof Method
Prove { x = 0 } x = x+1; || x = x+2; { x = 3 }.
Proof: Let P1 = (x = 0 x = 2), Q1 = (x = 1 x = 3), P2 = (x = 0 ⋁ ⋁ ⋁x = 1), Q2 = (x = 2 x = 3).⋁ Step 2: we prove interference freedom.
{ P1 P2 } x := x + 1 { P2 }⋀{ P1 Q2 } x := x + 1 { Q2 }⋀{ P1 P2 } x := x + 2 { P1 } ⋀{ Q1 P2 } x := x + 2 { Q1 }⋀
End of Proof
Owicki-Gries Proof Method
Let P1 be bal := bal + dep.Let P2 be: if (bal > 1000) { credit := 1; } else { credit := 0; }
Prove the following Hoare triple:{ bal = B dep > 0 } ⋀P1 || P2 { bal = B + dep dep > 0 (credit = 1 => bal > 1000)}⋀ ⋀
Owicki-Gries Proof Method
P1: bal := bal + dep;
Proof outline of P1:{ bal = B dep > 0 } ⋀bal := bal + dep{ bal = B + dep dep > 0 } ⋀
P2: if (bal > 1000) { credit := 1; } else { credit := 0; }
Proof outline of P2: { T }if (bal > 1000) {{ bal > 1000 } credit := 1;{ credit = 1 => bal > 1000 }}else {{ true } credit := 0;{ credit = 1 => bal > 1000 }}{ credit = 1 => bal > 1000 }
Owicki-Gries Proof MethodProve the proof of P1 is not interfering the proof of P2:
{ bal = B dep > 0 bal > 1000} ⋀ ⋀bal := bal + dep{ bal > 1000 }
{ bal = B dep > 0 true } ⋀ ⋀bal := bal + dep{ true }
{ bal = B dep > 0 credit = 1 => bal > 1000 } ⋀ ⋀ bal := bal + dep{credit = 1 => bal > 1000 }
Prove the proof of P2 is not interfering the proof of P1:
{ bal > 1000 bal = B dep > 0 }⋀ ⋀ credit := 1;{ bal = B dep > 0 }⋀
{ bal > 1000 bal = B + dep dep > 0 }⋀ ⋀ credit := 1;{bal = B + dep dep > 0 }⋀
{ bal = B dep > 0 }⋀ credit := 0;{ bal = B dep > 0 }⋀
{ bal = B + dep dep > 0 }⋀ credit := 0;{bal = B + dep dep > 0 }⋀
Auxiliary Variables
Let P be a program and A be a set of variables in P. A is a set of auxiliary variables of P if• variables in A occur only in assignments, not in
assignment guards or tests in loops or conditionals
• If x A occurs in an assignment (x1,...,xn) := ∈(E1,...,En) then x occurs in Ei only when xi A, ∈i.e., variables in A cannot influence variables outside A
Auxiliary Variable Rule
if there is a set A of auxiliary variables of P such that• P’ is obtained by removing variables in A (and
the relevant assignments) from P• post does not mention any variable in A.
{ pre } P { post }{ pre } P’ { post }
Exercise 2
Prove: {x=0} x=x+1 || x=x+1 {x=2}
To prove the above, we introduce two auxiliary variables, done1 and done2. We prove the following instead. {x=0 done1=0 done2=0} ⋀ ⋀ (x, done1 := x+1, 1) || (x, done2 := x+1, 1) {x=2}
Assume that each statement is atomic for now.
Owicki-Gries Proof Method
Prove: {x=0} (x, done1 := x+1, 1) || (x, done2 := x+1, 1) {x=2}
Proof outline Δ1:{ done1 = 0 (done2 = 0 => x = 0) (done2 = 1 => x = 1) }⋀ ⋀(x,done1) := (x+1,1){ done1 = 1 (done2 = 0 => x = 1) (done2 = 1 => x = 2) }⋀ ⋀
Proof outline Δ2:{ done2 = 0 (done1 = 0 => x = 0) (done1 = 1 => x = 1) }⋀ ⋀(x,done2) := (x+1,1){ done2 = 1 (done1 = 0 => x = 1) (done1 = 1 => x = 2) }⋀
Exercise 3
Prove: {x=0} (x, done1:=x+1, 1) || (x, done2:=x+1, 1) {x=2}
…Prove outline Δ1 and Δ2 are interference-free.
Recap: Owicki-Gries Method
• It requires to reason on the whole program. – Firstly, find “local” proof outlines– Second, check interference-freeness, which is not
local!
Can we come up with a way of proving concurrent programs locally?
Rely-Guarantee Reasoning
If you program that part … and provide me this interface … I will do this part … and provide an interface so that you can … as long as you don’t do that, then my part would work like this ...
Another Look
• Sequential proof: as long as the environment is guaranteed not to modify my variables, the proof holds.
• Owicki-Gries proof: as long as the environment is guaranteed not to invalidate my local proof outline, my local proof holds.
• Rely-Guarantee proof: for my local proof, I would specify exactly what assumptions that I make on the environment and what guarantee that I provide so that as long as the environment satisfies my assumption, my local proof stands.
Another Look
• Consider again: { x = 0 } x := x + 1 || x := x + 2 { x = 3 }
• In the example, the transition of P2, x := x + 2, (which is the environment of P1), is constrained by the predicate (x = 0 x’ = 2) (x = 1 x’ = 3) ⋀ ⋁ ⋀where x and x’ refer to program states before and after a transition.
• This fact suffices to prove that the assertion used in P1's local proof are not invalidated by interference from P2.
Rely-Guarantee Specification
A rely/guarantee specification is a quadruple ( pre, rely, guar, post )
• pre is the precondition, a single state predicate that describe what is assumed about the initial state;
• post is the post-condition, a two-state predicate relating the initial state to the final state immediately after the program terminates;
• rely is the rely-condition, which models all the atomic actions of the environment, describing the interference the program can tolerate from its environment.
• guar is the guarantee condition, which models the atomic actions of the program, and hence it describes the interference that it imposes on the other threads of the system.
Stability
A rely/guarantee specification is a quadruple (pre, rely, guar, post)
• In a specification (pre, rely, guar, post) we require that pre is stable under the rely condition, that is, they are resistant to interference from the environment.
• A predicate P is stable under R iff { P } R { P }. • For example: the predicate P = bal > 1000 dep > 0 ⋀
is stable under the action bal = bal + dep.
Rely-Guarantee
We write P |= ( pre, rely, guar, post ) to denote program P satisfies post while guaranteeing guar, given pre and rely are satisfied.
Compare {pre} P {post} and P |= (pre, rely, guar, post).
Rely-Guarantee Proof
Key ideas:• the action of the interleaved transitions of the other threads
(e.g. the states σi+1 and σj+1) is constrained by the rely condition;
• the post-condition relates the initial and final state, under the assumption that all other threads respect the rely constraints.
Rely-Guarantee Proof Rules
where q is: (post1; (rely1 rely2)*; post2) ⋀ ⋁(post2; (rely1 rely2)*; post1) ⋀
P1 |= ( pre1, rely1, guar1, post1 )P2 |= ( pre2, rely2, guar2, post2 )
guar1 => rely2guar2 => rely1
P1 || P2 |= (pre1 pre2, rely1 rely2, guar1 guar2, q)⋀ ⋀ ⋁
Are guar1 and guar2 relevant to the conclusion?
Rely-Guarantee Proof Rules
Proving a parallel program reduces to:• a sequential proof of the post-condition and guarantee condition
of each individual thread, assuming that its rely condition is true;• a pairwise proof that every other thread's guarantee condition
implies this thread's rely condition.
P1 |= ( pre1, rely1, guar1, post1 )P2 |= ( pre2, rely2, guar2, post2 )
guar1 => rely2guar2 => rely1
P1 || P2 |= (pre1 pre2, rely1 rely2, guar1 guar2, q)⋀ ⋀ ⋁
Rely-Guarantee Proof Rules
where• P is an atomic step;• preserve(p) means that for any action C from
the environment, {p}C{p}; • ID is the identify relation.
{ pre } P { post }
P |= ( pre, preserve(pre), post ID, post )⋁
Rely-Guarantee Proof Rules
For sequential composition:• The precondition of the second operand must follow from the
post-condition of the first. • The total action is given by the composition of the actions of its
components accounting for environment interference in between.
P1 |= ( pre1, rely, guar, post1 )P2 |= ( pre2, rely, guar, post2 )
post1 => pre2
P1; P2 |= (pre1, rely, guar, (post1; rely*; post2))
Rely-Guarantee Proof Rules
It is always safe to• strengthen the precondition or rely-condition• or weaken the post-condition or guarantee-condition
P |= ( pre, rely, guar, post )pre’ => prerely’ => rely
guar => guar’post => post’
P |= (pre’, rely’, guar’, post’)
ExampleProve: { x = 0 } x := x + 1 || x := x + 2 { x = 3 }
ProofWe prove the following about the first thread.x := x + 1 ⊨(x=0 x=2,⋁(x=0 x’=2) (x=1 x’=3) (x’=x),⋀ ⋁ ⋀ ⋁(x=0 x’=1) (x=2 x’=3) (x’=x),⋀ ⋁ ⋀ ⋁(x=0 x’=1) (x=2 x’=3))⋀ ⋁ ⋀
We show x=0 x=2 is stable under (x=0 x’=2) (x=1 x’=3) (x’=x). ⋁ ⋀ ⋁ ⋀ ⋁
(to be continued)
ExampleProve: { x = 0 } x := x + 1 || x := x + 2 { x = 3 }
ProofWe then prove the following on the second thread.x := x + 2 ⊨(x=0 x=1, ⋁(x=0 x’=1) (x=2 x’=3) x’=x, ⋀ ⋁ ⋀ ⋁(x=0 x’=2) (x=1 x’=3) (x’=x), ⋀ ⋁ ⋀ ⋁(x=0 x’=2) (x=1 x’=3))⋀ ⋁ ⋀
We show x=0 x=1 is stable under (x=0 x’=1) (x=2 x’=3) (x’=x).⋁ ⋀ ⋁ ⋀ ⋁
(to be continued)
Example
Prove: { x = 0 } x := x + 1 || x := x + 2 { x = 3 }
ProofWe then prove the following (guar1 => rely2):(x=0 x’=1) (x=2 x’=3) (x’=x) => (x=0 x’=1) (x=2 x’=3) (x’=x) ⋀ ⋁ ⋀ ⋁ ⋀ ⋁ ⋀ ⋁
We then prove the following (guar2 => rely1):(x=0 x’=2) (x=1 x’=3) (x’=x) => (x=0 x’=2) (x=1 x’=3) (x’=x)⋀ ⋁ ⋀ ⋁ ⋀ ⋁ ⋀ ⋁
(to be continued)
ExampleProve: { x = 0 } x := x + 1 || x := x + 2 { x = 3 }
ProofApply the rely-guarantee proof rule.
x := x + 1 || x := x + 2 |= ( (x=0 x=2) (x=0 x=1), ⋁ ⋀ ⋁ ((x=0 x’=2) (x=1 x’=3) x’=x) ((x=0 x’=1) (x=2 x’=3) x’=x) , ⋀ ⋁ ⋀ ⋁ ⋀ ⋀ ⋁ ⋀ ⋁ (x=0 x’=1) (x=2 x’=3) (x’=x) (x=0 x’=2) (x=1 x’=3) (x’=x), ⋀ ⋁ ⋀ ⋁ ⋁ ⋀ ⋁ ⋀ ⋁ q)
(to be continued)
ExampleProve: { x = 0 } x := x + 1 || x := x + 2 { x = 3 }
ProofApply the rely-guarantee proof rule. Notice that rely1 rely2 is equivalent to x’=x;⋀
x := x + 1 || x := x + 2 |= ( x=0, x’=x, guar, ((x=0 x’=1) (x=2 x’=3); (x’=x)*; (x=0 x’=2) (x=1 x’=3)) ⋀ ⋁ ⋀ ⋀ ⋁ ⋀ ⋁ ((x=0 x’=2) (x=1 x’=3); (x’=x)*; (x=0 x’=1) (x=2 x’=3))⋀ ⋁ ⋀ ⋀ ⋁ ⋀)
(to be continued)
ExampleProve: { x = 0 } x := x + 1 || x := x + 2 { x = 3 }
ProofApply the rely-guarantee proof rule. Notice that rely1 rely2 = false, which is ⋀equivalent to x’=x;
x := x + 1 || x := x + 2 |= ( x=0, x’=x, true, x = 0 x’ = 3 ⋀)
End of Proof.
x = 3
Exercise 4
Let P1 be the following program bal := bal + dep
Let P2 be the following programif bal > 1000 then credit := 1 else credit := 0
Prove the following: P1 || P2 ⊨{ dep > 0,dep’ = dep credit’ = credit bal’ = bal,⋀ ⋀true,bal’ = bal + dep dep > 0 (credit = 1 => bal > 1000) }⋀ ⋀
Example
Prove that given an array v[0..n-1] and a function P, write a function findp() to find the smallest r such that P(v[r]) = true holds.
public int findp(int[] v) { int N = v.length; for (int i = 0; i < N; i++) { if (P(v[i])) {
return i; } }}
public Boolean P(int n) { …}
Example
We prove the following using methods we discussed previously:{ i . P(v[i])) is defined }∀findp(v){ (r = n + 1 i . ¬P(v[i])) ⋀∀
(1 ≤ r ≤ n P(v[r]) i ⋁ ⋀ ⋀∀< r. ¬P(v[i]))) }
public int findp(int[] v) { int N = v.length; for (int i = 0; i < N; i++) { if (P(v[i])) {
return i; } }}
public Boolean P(int n) { …}
Example
Idea:• partition the array,• multiple processes search concurrently, one process per
partition.• Simple way: even and odd processes.
Naive concurrency: each process searches a partition, calculates the final result• as the minimum of the result of the even and odd processes.• Problem: can perform worse than sequential (why?)
Example
What is the RG specification for each thread in this program?
• pre: i. P(v[i])) is defined∀• rely: v’ = v top ≤ top⋀• guar: top’ = top top’ < ⋁
top P(v[top])⋀• post: i partition, i ≤ ∀ ∈
top => ¬P(v[i])
int top = v.length;
Thread 1: for (int i = 0; i < top; i=i+2) { if (P(v[i])) {
top = min(i,top); return;
} }
Thread 2: for (int i = 1; i < top; i=i+2) { if (P(v[i])) {
top = min(i,top); return;
} }
Summary
• Proving the correctness of multi-threaded programs are extremely difficult.
• Researchers are proposing proof rules and building proof systems based on those rules.
How do we learn what a programmer has in mind so as to generate guarantee-conditions and rely-conditions so that we can prove those program automatically?