590 likes | 692 Views
Proving Data Race Freedom in Relaxed Memory Models. Beverly Sanders. Computer & Information Science & Engineering. Overview. Motivation Memory models and data races Simple multi-threaded programming language and logic Extensions to allow reasoning about data races Example
E N D
Proving Data Race Freedom in Relaxed Memory Models Beverly Sanders Computer & Information Science & Engineering
Overview • Motivation • Memory models and data races • Simple multi-threaded programming language and logic • Extensions to allow reasoning about data races • Example • Conclusions and future work
Concurrent programming is hard • non-deterministic • non-reproducible • operational reasoning unreliable • It is becoming more commonplace • It is getting harder—relaxed memory models
Sequential consistency • Virtually all approaches for reasoning about concurrent program, both formal and informal assume SC • Lamport(1979) • All operations appear to execute in some sequential order • All operations on a single thread appear to execute in the order specified by the program
Modern systems are not SC • Optimizations (both by compiler and hardware) that preserve semantics of sequential programs may violate SC • reorder statements • buffered writes • …
Example • Initially: • int x = 0; • boolean done = false • Thread 1: • int r; • … • while (!done){/*busywait*/} • r := x; • … Thread 0: … x := …. done := true; …
Example Statements reordered. OK in sequential program but not in this one • Initially: • int x = 0; • boolean done = false • Thread 1: • int r; • … • while (!done){/*busywait*/} • r := x; • … Thread 0: … done := true; x := …. …
Loop optimized by compiler. Ok in sequential program but not this one Example • Initially: • int x = 0; • boolean done = false • Thread 1: • … • r0 = done; • while (!r0){/*busywait*/} • r := x; • … Thread 0: … x := …. done := true; …
Solutions? • Bad solution: require system to implement SC • unacceptable loss of performance • Better: provide mechanisms for the programmer to constrain non-SC behavior • explicit “fence” or “memory barrier” instructions • intrinsic • lock and unlock instructions • volatile variables • But then we should be able to verify that program is sufficiently constrained
Memory model • Specification of how threads interact with the memory • Traditionally, memory models have been specified for architectures • Recently, memory models have become part of the programming language semantics. • Java is the most ambitious attempt to date • Seems simple at first glance, but experience has shown it is hard to understand
Simple memory model • Based on the happened-before relation (Lamport) • Inspired by the Java Memory Model • an event happens-before another event on the same thread if it occurs earlier in the program order. • writing a volatile variable happens-before subsequent reads of the variable • unlocking a lock happens-before subsequent locks on the same lock • happened-before is transitive
Thread 0 Thread 1 write x write done read done read x happened before edge
Thread 0 Thread 1 done is volatile write x write done read done read x happened before edge
Thread 0 Thread 1 done is volatile write x write done read done read x happened before edge
Definition of data race • Conflicting accesses to the same variable that are not ordered by happens-before • Accesses to a variable conflict if they are performed by different threads and at least one is a write • Remark: term “race” is overloaded. • Sometimes it means any undesirable concurrency caused nondeterminism (which often comes from not using locks properly to enforce atomicity) • A program with no data races may still exhibit undesirable non-determinism.
Thread 0 Thread 1 write x write done read done read x happened before edge data race
Thread 0 Thread 1 done is volatile write x write done read done read x happened before edge
Fundamental property • If there are no data races in any sequentially consistent execution, then the program will behave as if it is sequentially consistent.
What about programs with data races? • Some systems leave behavior undefined • The Java memory model also constrains the behavior of programs with data races • includes a notion of causality • no "out of thin air" values • safety • Our goal is to prove the absence of data races, so we are not concerned with the behavior of programs with data races
Difficulties • It is difficult to reason about partial orders imposed on execution paths • Need data race freedom on all paths • In most work, one settles for satisfying sufficient constraints • all accesses to a particular shared variable are protected by a common lock • variable is volatile • Even "simple" rules are difficult to get right • Rules out important programming idioms
Proving data race freedom • Extend known assertional methods • Start with very simple multithreaded programming language (similar to Plato and BoogiePL) • Extend the state space with a "happened-before" function that tracks information about happened-before edges • Race-free access can be expressed as assertion • Prove in the usual way
Simple multithreaded programming language Program ::= Global* Volatile* Thread+ Thread ::= ThreadID Local* Stmt Stmt ::= … (no procedures or objects)
Stmt ::= Var ::= E | assume E | assert E | havoc Var+ | skip | Stmt [] Stmt | Stmt ; Stmt | < Stmt >
If E's value is true, continue, otherwise computation gets stuck. Getting stuck is OK Stmt ::= Var ::= E | assume E | assert E | havoc Var+ | skip | Stmt [] Stmt | Stmt ; Stmt | < Stmt >
If E's value is true, continue, otherwise computation "goes wrong". Going wrong is NOT OK Stmt ::= Var ::= E | assume E | assert E | havoc Var+ | skip | Stmt [] Stmt | Stmt ; Stmt | < Stmt >
Set the values of the given Vars to arbitrary values of their types Stmt ::= Var ::= E | assume E | assert E | havoc Var+ | skip | Stmt [] Stmt | Stmt ; Stmt | < Stmt >
Nondeterministic choice Stmt ::= Var ::= E | assume E | assert E | havoc Var+ | skip | Stmt [] Stmt | Stmt ; Stmt | < Stmt >
Sequential composition Stmt ::= Var ::= E | assume E | assert E | havoc Var+ | skip | Stmt [] Stmt | Stmt ; Stmt | < Stmt >
Statement is executed atomically Stmt ::= Var ::= E | assume E | assert E | havoc Var+ | skip | Stmt [] Stmt | Stmt ; Stmt | < Stmt >
This language is more expressive than it appears at first glance if E then S0 else S1 can be represented as assume E ; S0 [] assume ~E ; S1
while E do S with invariant I, where S modifies only variables in M assert I; havoc M; assume I; assume E ; S0; assert I; assume false; [] assume ~E;
Weakest preconditions wp.v := e.Q ≡ [e\v]Q wp.assume e.Q ≡ wp.assert e.Q ≡ e /\ Q wp.havoc v.Q ≡ wp.skip.Q ≡ Q wp.s0 [] s1.Q ≡ wp.s0.Q /\ sp.s1.Q wp.s0 ; s1.Q ≡ wp.s0.(wp.s1.Q)
{P} S {Q} ≡ • wp.S.Q0 /\ Q1 = wp.S.Q0 /\ wp.S.Q1
Atomic Statements • Implicitly atomic statements • satisfy "at most once rule" • statement accesses as most one non-local variable at most once • Explicitly atomic statement < S > Assumption that must be satisfied by the implementation • Well formed program: all assign, assert, assume, and havoc commands are implicitly atomic or are contained in an explicit atomic statement
Locks • Can be specified using explicit atomic statements lck : ThreadId + free lck.lock = <assume lcl=free; lck := curr> lck.unlock = <assert lck = curr; lck := free> where curr = current thread
(SC) Multithreaded Correctness Thread0 {Initial} <S0> {P1} <S1> .. {Pn} <Sn> {true} Thread1 {Initial} <T0> {Q1} <S1> .. {Qm} <Sm> {true} • Show each thread's proof outline is valid • Show non-interference (Owicki-Gries)
(SC) Multithreaded Correctness • Non-interference: no assertion in one thread is violated by an action in another. • Need to check all assertions between atomic actions Thread0 {Initial} <S0> {P1} <S1> .. {Pn} <Sn> {true} Thread1 {Initial} <T0> {Q1} <S1> .. {Qm} <Sm> {true}
(SC) Multithreaded Correctness Thread0 {Initial} <S0> {P1} <S1> .. {Pn} <Sn> {true} Thread1 {Initial} <T0> {Q1} <S1> .. {Qm} <Sm> {true} Example: To show that S1 in Thread0 does not falsify Qm in the proof outline of Thread1 {P1 /\Qm}S1{Qm}
Extensions for memory-synchronization state let h: globals+volatile+thread → {globals+volatile+thread} where norace('th','v') means that thread th can access v without causing a data race
Updates to h h('v') := h('v') U h('t') h('t') := h('v') U h('v')
Modify programming language • add (function valued) ghost variable h • statements replaced with new statements that also read and/or update h • if the modified program does not "go wrong", it is free of data races
Volatile variables • reading a volatile variable <acquire (curr,'x‘); r := x;> • writing a volatile variable x <x := e; release(curr,'x')>
Global (non-volatile) variables • reading global n < assert norace(curr, 'n'); r := n; > • writing global n < assert norace(curr,'n'); n := r; invalidate(curr,'n') >
Locks • Lock the lock variable lck <acquire(curr,'lck'); lck.lock;> • Unlock lck <lck.unlock; release(curr,'lck')>
Proof rules {norace('t1','y') \/ ('t0'='t1' /\ norace('x','y'))} acquire('t0','x'); {norace('t1','y')} {norace('t1','y') \/ ('x'='t1' /\ norace('t0','y'))} release('t0','x'); {norace('t1','y')}
Proof rules {norace('t1','y') /\ ('t0' = 't1' \/ 'x' ≠ 'y')} invalidat('t0','x') {norace('t1','y')}
Recall example • Initially: • int x = 0; • boolean done = false • Thread 1: • int r; • … • while (!done){/*busywait*/} • r := x; • … Thread 0: … x := …. done := true; …
Thread 0: {norace('Thread0','done') /\ norace('Thread0','x') <assert norace(Thread0’,'x'); x := 1; invalidate(Thread0’,'x') > {norace(Thread0,done)} <assert norace(‘Thread0’,'done'); done := true; invalidate(‘Thread0’,'done'); > {true} done not volatile
Thread 1: r0 := done; assume !r0; assume false; [] assume r0; r := x Thread 1: int r; … while (!done){} r := x; …