710 likes | 827 Views
A Statically Verifiable Programming Model for Concurrent Object-Oriented Programs. Bart Jacobs 1 (≠ Prof. B. P. F. Jacobs, R.U.Nijmegen, The Netherlands) Joint work with Jan Smans 1 , Frank Piessens 1 , Wolfram Schulte 2 , Rustan Leino 2 1 K.U.Leuven, Belgium
E N D
A Statically Verifiable Programming Model for Concurrent Object-Oriented Programs Bart Jacobs1 (≠ Prof. B. P. F. Jacobs, R.U.Nijmegen, The Netherlands) Joint work with Jan Smans1, Frank Piessens1, Wolfram Schulte2 , Rustan Leino2 1 K.U.Leuven, Belgium 2 Microsoft Research, Redmond, WA, USA
Outline of the Talk • Progressive refinement of the approach: • Absence of data races • Absence of deadlocks • Ownership • Object invariants • Immutable objects • Related work, future work, conclusion
The Problem • Goal: Delivering correct concurrent Java programs • Problem: It’s hard to reason about such programs • due to the non-local nature of data races, deadlocks, and object aliasing • Proposed solution: • A programming regime (or programming model) • that prevents data races • and deadlocks, • and enables local reasoning in the presence of object aliasing. • An annotation syntax and verification approach • that enables modular static verification of compliance with the programming model.
Outline of the Talk • Progressive refinement of the approach: • Absence of data races • Absence of deadlocks • Ownership • Object invariants • Immutable objects • Related work, future work, conclusion
Data Races class Account { int balance; } Account act = …; int b0 = act.balance; act.balance += 50; int b1 = act.balance; b1 == b0 + 50? Not necessarily!
Data Races class Account { int balance; } Account act = …; int b0, b1; synchronized (act) { b0 = act.balance; act.balance += 50; b1 = act.balance; } b1 == b0 + 50? Not necessarily!
class Account { int balance; } Account act = …; int b0, b1; synchronized (act) { b0 = act.balance; act.balance += 50; b1 = act.balance; } b1 == b0 + 50? Not necessarily! In Java, it’s not sound to reason sequentially about sequential code due to the possibility of data races. Data Races
Preventing data races: Access sets • Per-thread access set t.A • x = o.f; or o.f = x; requires o in t.A • Always: t1 != t2 ==> t1.A and t2.A disjoint • Object can move between access sets only via proper synchronization
Preventing data races: Access sets • x := new C; implies x in t.A’ • entering synchronized (o) implies o in t.A’ • exiting synchronized (o) implies o not in t.A’
Preventing data races: Access sets • x := new C; implies x in t.A’ • entering synchronized (o) implies o in t.A’ • exiting synchronized (o) implies o not in t.A’ • But: races between creating thread and locking thread?
Preventing data races:Shared and unshared objects • x := new C; implies x is unshared’ • attempting synchronized (o) requires x is shared • programmer indicates share o; • requires o in t.A and o is unshared • implies o is not in t.A’ and o is shared’ • shared objects never become unshared again • Property: If a shared object o is not locked by any thread, then it is not in any thread’s access set.
Preventing data races // thread 1: x := new C; x.f := 5; share x; // start thread 2 // thread 2: synchronized (x) { int n := x.f; } shared t1.A
Preventing data races // thread 1: x := new C; x.f := 5; share x; // start thread 2 // thread 2: synchronized (x) { int n := x.f; } shared t1.A x
Preventing data races // thread 1: x := new C; x.f := 5; share x; // start thread 2 // thread 2: synchronized (x) { int n := x.f; } shared t1.A x
Preventing data races // thread 1: x := new C; x.f := 5; share x; // start thread 2 // thread 2: synchronized (x) { int n := x.f; } shared t1.A x
Preventing data races // thread 1: x := new C; x.f := 5; share x; // start thread 2 // thread 2: synchronized (x) { int n := x.f; } shared t1.A x t2.A
Preventing data races // thread 1: x := new C; x.f := 5; share x; // start thread 2 // thread 2: synchronized (x) { int n := x.f; } shared t1.A x t2.A
Preventing data races // thread 1: x := new C; x.f := 5; share x; // start thread 2 // thread 2: synchronized (x) { int n := x.f; } shared t1.A x t2.A
Preventing data races // thread 1: x := new C; x.f := 5; share x; // start thread 2 // thread 2: synchronized (x) { int n := x.f; } shared t1.A x t2.A
Thread creation • new Thread(r).start() • requires r in t.A • implies r not in t.A’ • in new thread: r in t.A
Preventing Data Races: Lock Re-entry • Causes difficulties for method effect framing • We disallow it
Preventing data races:Modular Static Verification • Annotations required: • Share commands • Method contracts • requires/ensures o is in tid.A (tid = current thread) • requires/ensures o is unshared/shared • requires/ensures o is not in tid.lockset/tid.lockset is empty • Field modifier: shared • Verification approach: • Verification condition generation • On entry to synchronized block: • foreach (o.f where o not in t.A) { o.f := random; } • Method effect framing: required access sets
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example main main.A shared
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example main main.A shared c
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example main main.A shared c
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example main main.A shared s1 c
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example main main.A shared s1 c
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example main s1 main.A shared c s1 s1.A
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example main s1 main.A shared s2 c s1 s1.A
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example main s1 main.A shared s2 c s1 s1.A
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example s2 s1 main.A shared c s1 s2 s2.A s1.A
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example s2 s1 main.A shared c s1 s2 s2.A s1.A
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example s2 main.A s1 shared c s1 s2 s2.A s1.A
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example s2 main.A shared c s1 s2 s2.A s1.A
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example s2 main.A shared c s1 s2 s2.A s1.A
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example main.A s2 shared c s1 s2 s2.A s1.A
class Counter { int count; } class Session implements Runnable { shared Counter counter; public void run() requires this in tid.A; requirestid.lockset is empty; { synchronized (counter) { counter.count++; } } } Counter counter = new Counter(); share counter; Session session1 = new Session(); session1.counter = counter; new Thread(session1).start(); Session session2 = new Session(); session2.counter = counter; new Thread(session2).start(); Preventing data races: Example main.A shared c s1 s2 s2.A s1.A
Outline of the Talk • Progressive refinement of the approach: • Absence of data races • Absence of deadlocks • Ownership • Object invariants • Immutable objects • Related work, future work, conclusion
Preventing deadlocks • Deadlock = cycle of threads each waiting for the next to release a lock • Proposed solution: • Programmer constructs partially ordered set of lock levels • using l := between({ll1,...,lln},{lu1,...,lum}); annotation • Assigns a lock level to each shared object • A thread may attempt to acquire an object’s lock only if the object is below the objects whose lock it already holds
Preventing deadlocks: example class Fork {} class Philosopher implements Runnable { shared Fork fa, fb; Philosopher(Fork fa, Fork fb) requires fa.locklevel < fb.locklevel; { this.fa = fa; this.fb = fb; } public void run() requiresthis in tid.A and lockset is empty; { synchronized (fb) { synchronized (fa) { /* eat */ } } } } Fork f1 = new Fork(); locklevel l1 = between({},{}); share f1 at l1; Fork f2 = new Fork(); locklevel l2 = between({l1},{}); share f2 at l2; Fork f3 = new Fork(); locklevel l3 = between({l2},{}); share f3 at l3; new Thread(new Philosopher(f1, f2)).start(); new Thread(new Philosopher(f2, f3)).start(); new Thread(new Philosopher(f1, f3)).start();
Outline of the Talk • Progressive refinement of the approach: • Absence of data races • Absence of deadlocks • Ownership • Object invariants • Immutable objects • Related work, future work, conclusion
Rep Objects • Objects often use auxiliary objects to help represent their state • e.g. an ArrayList uses an array object • We wish to protect the array object against data races using the lock of the ArrayList object • Proposed solution: use Spec#’s ownership system • Fields may be marked with rep modifier • The objects pointed to by o’s rep fields are its rep objects • An object may be packed or unpacked • When an object is in the packed state, it owns its rep objects • A thread can gain access to an owned object by locking the owner and then unpacking the owner
Ownership system • When packing an object o, using a pack o; annotation, o’s rep objects are removed from tid.A • When unpacking o, using an unpack o; annotation, o’s rep objects are added to tid.A
Ownership system: Example class BoundedList { rep Object[] elements; int count; BoundedList(int capacity) ensuresthis is in tid.A and this is packed; { elements = new Object[capacity]; pack this; } void add(Object value) requiresthis is in tid.A and this is packed; ensuresthis is in tid.A and this is packed; { unpack this; elements[count++] = value; pack this; } } BoundedList bl = new BoundedList(); share bl; synchronized (bl) { bl.add(null); }
Ownership system: Example class BoundedList { rep Object[] elements; int count; BoundedList(int capacity) ensuresthis is in tid.A and this is packed; { elements = new Object[capacity]; pack this; } void add(Object value) requiresthis is in tid.A and this is packed; ensuresthis is in tid.A and this is packed; { unpack this; elements[count++] = value; pack this; } } BoundedList bl = new BoundedList(); share bl; synchronized (bl) { bl.add(null); } shared t.A
Ownership system: Example class BoundedList { rep Object[] elements; int count; BoundedList(int capacity) ensuresthis is in tid.A and this is packed; { elements = new Object[capacity]; pack this; } void add(Object value) requiresthis is in tid.A and this is packed; ensuresthis is in tid.A and this is packed; { unpack this; elements[count++] = value; pack this; } } BoundedList bl = new BoundedList(); share bl; synchronized (bl) { bl.add(null); } shared t.A bl
Ownership system: Example class BoundedList { rep Object[] elements; int count; BoundedList(int capacity) ensuresthis is in tid.A and this is packed; { elements = new Object[capacity]; pack this; } void add(Object value) requiresthis is in tid.A and this is packed; ensuresthis is in tid.A and this is packed; { unpack this; elements[count++] = value; pack this; } } BoundedList bl = new BoundedList(); share bl; synchronized (bl) { bl.add(null); } shared t.A bl es
Ownership system: Example class BoundedList { rep Object[] elements; int count; BoundedList(int capacity) ensuresthis is in tid.A and this is packed; { elements = new Object[capacity]; pack this; } void add(Object value) requiresthis is in tid.A and this is packed; ensuresthis is in tid.A and this is packed; { unpack this; elements[count++] = value; pack this; } } BoundedList bl = new BoundedList(); share bl; synchronized (bl) { bl.add(null); } shared t.A bl es
Ownership system: Example class BoundedList { rep Object[] elements; int count; BoundedList(int capacity) ensuresthis is in tid.A and this is packed; { elements = new Object[capacity]; pack this; } void add(Object value) requiresthis is in tid.A and this is packed; ensuresthis is in tid.A and this is packed; { unpack this; elements[count++] = value; pack this; } } BoundedList bl = new BoundedList(); share bl; synchronized (bl) { bl.add(null); } shared t.A bl es