620 likes | 750 Views
This paper discusses techniques for verifying concurrent data structures using optimistic concurrency. Key concepts include proofs of linearizability, the significance of history variables, and the use of prophecy variables to refer to future states in programs. The authors present a compositional method for verifying software transactional memory implementations, leveraging static proof systems like QED. Through examples and static reasoning, the talk explores challenges and methodologies for ensuring correctness in optimistic concurrency, emphasizing reduced interference and backward reasoning strategies.
E N D
Verifying Optimistic Concurrency: Prophecy Variables and Backwards Reasoning SerdarTasiran Koç University Istanbul, Turkey TayfunElmasShaz Qadeer Ali SezginKoç UniversityMicrosoft ResearchKoç University Istanbul, Turkey Redmond, WAIstanbul, Turkey
The .1% Problem(but cute!) SerdarTasiran Koç University Istanbul, Turkey TayfunElmasShaz Qadeer Ali SezginKoç UniversityMicrosoft ResearchKoç University Istanbul, Turkey Redmond, WAIstanbul, Turkey
The Gist • Proofs of linearizability and refinement for concurrent data structures • Verifying the critical 1% • Experience: • Lipton-style reduction and abstraction really help. • History variables are often needed • For data structures using optimistic concurrency (STM’s, etc.) proofs need to refer to the future • Prophecy variables • This talk • QED + prophecy variables • Thinking backwards in time
Static Reduction Proofs for Optimistic Concurrency • Goal: Verify programs that use optimistic concurrency • “A Compositional Method for Verifying Software Transactional Memory Implementations” • Partly mechanically checked • Reduction Coarser atomic blocks • Optimistic concurrency:Proceed assuming non-interference • Abort, undo and/or retry if interference detected • Approach: Static proof system QED [POPL ‘09, PADTAD ‘09] • Challenge in applying QED to optimistic concurrency: • Same code, different reduction proofs for different futures • Need mechanism for referring to execution’s future • Prophecy variables and backwards reasoning in QED
Outline • Forward reasoning overview • Lock-protected increment • QED proof • History variables, “assert” annotations • Backward reasoning: Temporal dual • Optimistic concurrency example: Copy • Why forward reasoning doesn’t work • Temporally dual approach • Prophecy variables, “tressa” annotations • Generalized (back and forth) QED • Examples
History Variable Annotations Make Static Mover Check Pass Thread 1 acquire (lock); a := tid1; assert a == tid1; t1 = x; t1 = t1 + 1 assert a == tid1; x = t1; assert a == tid1;release(lock); a := 0; Thread 2 acquire (lock); a := tid2; assert a == tid2; t2 = x; t2 = t2 + 1 assert a == tid2; x = t2; assert a == tid2;release(lock); a := 0; R B B B L • assert a == tid1; x = t1; andassert a == tid2; x = t2; commute • αβ≤ βα • Because αβ and βα result in assertion violations.
Borrowing and paying back assertions Invariant: (lock == true) (a != 0) Discharges the assertions inc (): int t; acquire (lock); a := tid; assert a == tid; t = x; t = t + 1 assert a == tid; x = t; assert a == tid;release(lock); a := 0; inc (): int t; acquire (lock); a := tid; assert a == tid;t = x; t = t + 1 assert a == tid;x = t; assert a == tid;release(lock); a := 0; R B B B L REDUCE & RELAX
Outline • Forward reasoning overview • Lock-protected increment • QED proof • History variables, “assert” annotations • Backward reasoning: Temporal dual • Optimistic concurrency example: Copy • Why forward reasoning doesn’t work • Temporally dual approach • Prophecy variables, “tressa” annotations • Generalized (back and forth) QED • Examples
Optimistic Concurrency Example Copy(fr: Obj, to: Obj){ atomic{ version := fr.ver; value := fr.val;} atomic{ if (version == fr.ver) {to.val := value; to.ver := to.ver + 1;} } } Wrt(to: Obj, newVal: int){ atomic{ to.val := newVal; to.ver := to.ver + 1;} }
Optimistic Concurrency Example Copy(fr: Obj, to: Obj){ action SS(fr): atomic{ version := fr.ver; value := fr.val;} action ConfNWrt(fr, to): atomic{ if (version == fr.ver) {to.val := value; to.ver := to.ver + 1;} } } Wrt(to: Obj, newVal: int){ atomic{ to.val := newVal; to.ver := to.ver + 1;} }
Optimistic Concurrency Example Copy(fr: Obj, to: Obj){ action SS(fr): atomic{ version := fr.ver; value := fr.val;} action ConfNWrt(fr, to): atomic{ if (version == fr.ver) {to.val := value; to.ver := to.ver + 1;} } } Wrt(to: Obj, newVal: int){ atomic{ to.val := newVal; to.ver := to.ver + 1;} } Goal: Prove that SS(fr) is a right mover
Reduction for Optimistic Concurrency • Copy(x, to_y) running concurrently withWrt(x) • Does SS always commute to the right of Wrt? • Failing Copy (Wrt interferes with Copy) T1: SS(x) ConfNWrt(x, to_y) T2:Wrt(x, val) • Succeeding Copy (No interference) T1: SS(x) ConfNWrt(x, to_y) T2: Wrt(x, val)
Reduction Argument for Optimistic Concurrency • Failing Copy T1: SS(x) ConfNWrt(x, to_y) T2:Wrt(x, val) • Snapshot taken but ConfNWrtdoes not write to to_y. • Abstract SS: action SS(fr): atomic{ version := fr.ver; value := fr.val;} action SS_Abs(fr): atomic{ havoc version, value; }
Copy Example After Abstraction Copy(fr: Obj, to: Obj){ action SS_Abs(fr): atomic{ havoc version, value; } action ConfNWrt(fr, to): atomic{ if (version == fr.ver) {to.val := value; to.ver := to.ver + 1;} } } Wrt(to: Obj, newVal: int){ atomic{ to.val := newVal; to.ver := to.ver + 1;} }
Problem: Too much abstraction • Succeeding Copy (No interference) T1: SS_Abs(x) ConfNWrt(x, to_y) T2: Wrt(x, val) • Arbitrary value written to to_y! • Want to abstract SS(x) toSS_Abs(x) only in executionsin which interference occurs (later). • But, SS_Abs doesn’t yet know whether interference will occur. • Need mechanism to refer to the future of the execution.
Prophecy Variables • Prophecy variable: Auxiliary variable, encodes future non-determinism • Allows actions to refer to future locally • Can use in annotations, abstraction. • Different reduction proofs for different futures • Concurrent systems: Non-determinism due to thread interleaving p = R, G or B
Introducing a Prophecy Variable action ConfNWrt(fr, to): atomic{ if (version == fr.ver) {to.val := value; to.ver := to.ver + 1;} } action ConfNWrt(fr, to): atomic{ if (version == fr.ver) { p =: true; to.val := value; to.ver := to.ver + 1; } else { p =: false; } } }
Backwards Assignment “=:” • Backwards assignment p =: x; • Shorthand for atomic{ assume p == x; havoc p;} • p should match x • p’ can have any value • Thinking backwards in time: Assigns the value of x to p • In the execution, up to the point the following action is taken if (version == fr.ver) { p =: true; to.val := value; to.ver := to.ver + 1; } else { p =: false;} p is true iff the version number check (later) succeeds.
Prophecy Variable Introduction: Soundness • Annotating actionα(s,s’) with prophecy variable p α(s,s’) becomesβ(s,p, s’,p’) • Must satisfy • p’. p. β(s,p, s’,p’) (History variables: h. h’. β(s,h, s’,h’) ) • Backwards assignment satisfies this • Soundness: • Every state of every execution can be annotated with a value of p. τ
Abstraction Constrained by Prophecy Variable action SS_P(x): atomic{ if (p) {version := x.ver; value := x.val;} else { havoc version, value;} }
Reduction Proof of Optimistic Concurrency • Succeeding Copy T1: SS(x) ConfNWrt(x, to_y) T2: Wrt(x, val) • SS(x) commutes to the right of Wrt(x, val) • Copy succeeds Wrt(x, val) never immediately follows SS(x) • Need to annotate SS(x) with this fact. • Similar case: assert a == tid1; x = t1; andassert a == tid2; x = t2; • These two actions cannot follow each other
Reduction Proof of Optimistic Concurrency • Copy succeeds Wrt(x, val) never immediately follows SS(x) • Need to annotate SS(x) with this fact. • But SS(x) doesn’t yet know whether this is a succeeding Copy. • Think backwards: • Walk back from the end of an execution, • When we reach s2, we know whether Copy has been successful. • Restated fact: If Copy succeeded, at s2, the version number snapshot should be up-to-date. SS(x) ConfNWrt(x) … … … s0 s1 s2
Putting it all together Copy(fr: Obj, to: Obj){ action SS_P(fr): atomic{ if (p) {version := fr.ver; value := fr.val;} else { havoc version, value;} tressa p ==> (version >= fr.ver); } action ConfNWrt(fr, to): atomic{ if (version == fr.ver) { p =: true; to.val := value; to.ver := to.ver + 1; } else { p =: false; } } }
tressa : Temporal Dual of assert • tressa: Mechanism to annotate actions with assertions that can refer to prophecy variables • assert: Discharged by reasoning about history of execution. • tressa: Temporal dual of assert • Example:y := y+1; z := z-1; assume (x == 0);
tressa : Temporal Dual of assert • Example:y := y+1; // x == 0 or execution blocksz := z-1; // x == 0 or execution blocks assume (x == 0); • But • atomic{ assert x == 0; y := y+1;} atomic{ assert x == 0; z := z-1;} assume (x == 0);does not work! • Cannot discharge the assertions!
tressa : Temporal Dual of assert • Example:y := y+1; // x == 0 or execution blocksz := z-1; // x == 0 or execution blocks assume (x == 0); • tressa φ: Either φ holds in the post state, or execution does not terminate (blocks). • atomic{ y := y+1; tressa x == 0;} atomic{ z := z-1; tressa x == 0;} assume (x == 0); • tressa annotations discharged by backwards reasoning within an atomic block. • Discharged tressaφ: You cannot come back from a final state of the program and violate φ
Discharging tressa’s inc (): int t; acquire (lock); p =: 0 tressa a == tid; t = x; t = t + 1 tressa a == tid; x = t; release(lock); p =: tid; inc (): int t; acquire (lock); p =: 0; tressa p == tid;t = x; t = t + 1 tressa a == tid;x = t; release(lock); p =: tid; R B B B L REDUCE & RELAX
Actions with assert’s and tressa’s • Canonical action α: {assert a1; τ1; tressa p1} • τ1( s, s’) : Transition predicate • a1(s): assert predicate on the pre-state • p1(s’): tressa predicate on the post-state • Operational semantics: • assert and tressa predicates have no effect on execution • Execution has assert violation if any αi’sassert predicate is violated. • Execution has tressa violation if any αi’stressa predicate is violated. • QED± preserves assert and tressa violations α0 α1 α2 αn-1 … s0 s1 s2 s3 sn-1 sn
Abstraction and Mover Checks with tressa’s abstracts {assert a1; τ1; tressa p1} ≤ {assert a2; τ2; tressa p2} • Preserve assert violations: a2 a1 • Preserve assert violations: p2 p1 • Forward simulate or replace with assert violation: τ1 (s,s’) τ2 (s,s’) ∨ a2(s) • Backward simulate orreplace with tressa violation: τ1 (s,s’) τ2 (s,s’) ∨ p2(s’) • Does α commute to the right of β ? αβ≤βα
Putting it all together Copy(fr: Obj, to: Obj){ action SS_P(fr): atomic{ if (p) {version := fr.ver; value := fr.val;} else { havoc version, value;} tressa p ==> (version >= fr.ver); } action ConfNWrt(fr, to): atomic{ if (version == fr.ver) { p =: true; to.val := value; to.ver := to.ver + 1; } else { p =: false; } } }
SS_P Wrt ≤ Wrt SS_P (Case 1) p == false Wrt(x) SS_P (x): havoc version, value action SS_P(x): atomic{ if (p) {version :=x.ver; value :=x.val;} else { havoc version, value;} tressa p ==> (version >= ver); } Wrt(x) { atomic{ x.val := newVal; x.ver := x.ver + 1;} }
SS_P Wrt ≤ Wrt SS_P (Case 1) p == false SS_P(x): havoc version, value Wrt(x) Wrt(x) SS_P (x): havoc version, value action SS_P(x): atomic{ if (p) {version :=x.ver; value :=x.val;} else { havoc version, value;} tressa p ==> (version >= ver); } Wrt(x) { atomic{ x.val := newVal; x.ver := x.ver + 1;} }
SS_P Wrt ≤ Wrt SS_P (Case 2) p == true Wrt(x) SS_Proph(x): tressa(version>=x.ver) action SS_P(x): atomic{ if (p) {version :=x.ver; value :=x.val;} else { havoc version, value;} tressa p ==> (version >= ver); } Wrt(x) { atomic{ x.val := newVal; x.ver := x.ver + 1;} }
SS_P Wrt ≤ Wrt SS_P (Case 2) p == true Wrt(x) SS_Proph(x): tressa(version>=x.ver) version < x.ver(Case 2) action SS_P(x): atomic{ if (p) {version :=x.ver; value :=x.val;} else { havoc version, value;} tressa p ==> (version >= ver); } Wrt(x) { atomic{ x.val := newVal; x.ver := x.ver + 1;} }
SS_P Wrt ≤ Wrt SS_P (Case 2) p == true version < x.ver Wrt(x) SS_Proph(x): tressa(version>=x.ver) version < x.ver(Case 2) action SS_P(x): atomic{ if (p) {version :=x.ver; value :=x.val;} else { havoc version, value;} tressa p ==> (version >= ver); } Wrt(x) { atomic{ x.val := newVal; x.ver := x.ver + 1;} }
SS_P Wrt ≤ Wrt SS_P (Case 2) p == true SS_P (x): tressa(version>=x.ver) version < x.ver Wrt(x) SS_Proph(x): tressa(version>=x.ver) version < x.ver(Case 2) action SS_P(x): atomic{ if (p) {version :=x.ver; value :=x.val;} else { havoc version, value;} tressa p ==> (version >= ver); } Wrt(x) { atomic{ x.val := newVal; x.ver := x.ver + 1;} }
SS_P Wrt ≤ Wrt SS_P (Case 3) p == true ✔ Wrt(x) SS_Proph(x): tressa(version>=x.ver) action SS_P(x): atomic{ if (p) {version :=x.ver; value :=x.val;} else { havoc version, value;} tressa p ==> (version >= ver); } Wrt(x) { atomic{ x.val := newVal; x.ver := x.ver + 1;} }
SS_P Wrt ≤ Wrt SS_P (Case 3) p == true ✔ Wrt(x) SS_Proph(x): tressa(version>=x.ver) version == x.ver action SS_P(x): atomic{ if (p) {version :=x.ver; value :=x.val;} else { havoc version, value;} tressa p ==> (version >= ver); } Wrt(x) { atomic{ x.val := newVal; x.ver := x.ver + 1;} }
SS_P Wrt ≤ Wrt SS_P (Case 3) p == true ✔ Wrt(x) SS_Proph(x): tressa(version>=x.ver) version+1 == x.ver version == x.ver action SS_P(x): atomic{ if (p) {version :=x.ver; value :=x.val;} else { havoc version, value;} tressa p ==> (version >= ver); } Wrt(x) { atomic{ x.val := newVal; x.ver := x.ver + 1;} }
SS_P Wrt ≤ Wrt SS_P (Case 3) p == true SS_Proph(x): tressa(version>=x.ver) Wrt(x) SS_Proph(x): tressa(version>=x.ver) version+1 == x.ver version == x.ver action SS_P(x): atomic{ if (p) {version :=x.ver; value :=x.val;} else { havoc version, value;} tressa p ==> (version >= ver); } Wrt(x) { atomic{ x.val := newVal; x.ver := x.ver + 1;} }
Copy is Atomic R Copy(fr: Obj, to: Obj){ action SS_P(fr): atomic{ if (p) {version := fr.ver; value := fr.val;} else { havoc version, value;} tressa p ==> (version >= ver); } action ConfNWrt(fr, to): atomic{ if (version == fr.ver) { p =: true; to.val := value; to.ver := to.ver + 1; } else { p =: false; } } }
Proving Optimistic Concurrency in QED • tressa’s allow one to express the following fact locally • If action b immediately follows action a, then this execution will eventually block • Optimistic concurrency proof template: • Introduce prophecy variables • Backwards assign them when non-determinism is resolved • Annotate earlier actions with tressa’s • Perform a case split in the proof based on possible futures • When blocks are large enough, discharge tressa’s by reasoning backwards in time.
Example 2: ReadPair procedure ReadPair(a: int, b: int) returns (s: bool, da: Obj, db: Obj) { varva: int, vb: int; 1: atomic { va := m[a].v; da := m[a].d; } 2: atomic { vb := m[b].v; db := m[b].d; } 3: s := true; 4: atomic { if (va < m[a].v) { s:= false; } } 5: atomic { if (vb < m[b].v) { s:= false; } } 6: if (!s) { da := nil; db := nil; } } procedure Write(a: int, d: Obj) { atomic { m[a].d := d; m[a].v := m[a].v+1; } }
Example 3: Multiset (Insert/Lookup) procedure Lookup(x: data) returns result: bool; { f := false; i := 0; while (i<n && !f) { f := (q[i] == x); i := i+1; } result = f; } procedure Insert(x: data) returns result: bool; { havoc i; assume i<n; cnt := 0; result := false; while (cnt<n && !f) { if (q[i]==-1) {q[i] := x; result := true; } else if (q[i]== x) { result := true; } else { i := (i+1) mod n; cnt := cnt+1; } } }
Example 3: Multiset (Insert/Lookup) procedure Lookup(x: data) returns result: bool; { … while (i<n && !f) { f := (q[i] == x); i := i+1; } result = f; } procedure Insert(x: data) returns result: bool; { ... } • Lookup’s that return true commit when they find x. • Lookup’s that return false commit when they read q[0]. • Different reduction proofs needed for the two cases • Replace Lookup(x) with if (*) { Lookup(x); assume result} else { Lookup(x); assume !result} • Use return value as prophecy variable • Perform different reduction proofs on the two branches
Summary • QED: Proof system for concurrent software • Iteration of abstraction and reduction surprisingly powerful • Intricate algorithms verified • Reduction proofs for optimistic concurrency • Actions need to refer to the future: prophecy variables • Annotate actions locally with info about future: tressa • QED + prophecy variables, tressa’s, forward & backward reasoning • Generalized definition of simulation • Soundness proof • Proved examples making use of optimistic concurrency • Future work: Verify transactional memory algorithms, non-blocking data structures.
Multiset 2 3 9 3 5 8 6 8 3 elt ✗ ✗ ✔ ✔ ✔ ✔ ✔ ✔ ✔ vld 49 LookUp (x) for i =1 to n acq (M[i]); if (M[i].elt==x && M[i].vld) rel (M[i]); return true; else rel (M[i]); return false; M ✗ • Multiset data structure M = { 2, 3, 3, 9, 8, 8, 5 } • Represented by M[1..n] • elt: The element • vld: Is it in the set?
FindSlot and InsertPair InsertPair (x,y) i=FindSlot (x); if (i == -1) { return failure; } j=FindSlot (y); if (j == -1) { M[i].elt= null; return failure; } acq (M[i]) acq (M[j]) M[i].vld = true; M[j].vld = true; rel (M[i]); rel (M[j]); return success; FindSlot (x) r = -1; i = 0; while(i < N && r == -1) { acq (A[i]); if (M[i].elt ==null) { M[i].elt= x; rel (M[i]); r = i; } else { rel (M[i]); } i = i + 1; } return r;