1 / 59

Proving Data Race Freedom in Relaxed Memory Models

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

Download Presentation

Proving Data Race Freedom in Relaxed Memory Models

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Proving Data Race Freedom in Relaxed Memory Models Beverly Sanders Computer & Information Science & Engineering

  2. 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

  3. Concurrent programming is hard • non-deterministic • non-reproducible • operational reasoning unreliable • It is becoming more commonplace • It is getting harder—relaxed memory models

  4. 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

  5. Modern systems are not SC • Optimizations (both by compiler and hardware) that preserve semantics of sequential programs may violate SC • reorder statements • buffered writes • …

  6. Example • Initially: • int x = 0; • boolean done = false • Thread 1: • int r; • … • while (!done){/*busywait*/} • r := x; • … Thread 0: … x := …. done := true; …

  7. 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 := …. …

  8. 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; …

  9. 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

  10. 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

  11. 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

  12. Thread 0 Thread 1 write x write done read done read x happened before edge

  13. Thread 0 Thread 1 done is volatile write x write done read done read x happened before edge

  14. Thread 0 Thread 1 done is volatile write x write done read done read x happened before edge

  15. 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.

  16. Thread 0 Thread 1 write x write done read done read x happened before edge data race

  17. Thread 0 Thread 1 done is volatile write x write done read done read x happened before edge

  18. Fundamental property • If there are no data races in any sequentially consistent execution, then the program will behave as if it is sequentially consistent.

  19. 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

  20. 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

  21. 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

  22. Simple multithreaded programming language Program ::= Global* Volatile* Thread+ Thread ::= ThreadID Local* Stmt Stmt ::= … (no procedures or objects)

  23. Stmt ::= Var ::= E | assume E | assert E | havoc Var+ | skip | Stmt [] Stmt | Stmt ; Stmt | < Stmt >

  24. 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 >

  25. 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 >

  26. 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 >

  27. Nondeterministic choice Stmt ::= Var ::= E | assume E | assert E | havoc Var+ | skip | Stmt [] Stmt | Stmt ; Stmt | < Stmt >

  28. Sequential composition Stmt ::= Var ::= E | assume E | assert E | havoc Var+ | skip | Stmt [] Stmt | Stmt ; Stmt | < Stmt >

  29. Statement is executed atomically Stmt ::= Var ::= E | assume E | assert E | havoc Var+ | skip | Stmt [] Stmt | Stmt ; Stmt | < Stmt >

  30. 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

  31. 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;

  32. 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)

  33. {P} S {Q} ≡ • wp.S.Q0 /\ Q1 = wp.S.Q0 /\ wp.S.Q1

  34. 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

  35. 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

  36. (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)

  37. (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}

  38. (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}

  39. 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

  40. Updates to h

  41. Updates to h h('v') := h('v') U h('t') h('t') := h('v') U h('v')

  42. 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

  43. Volatile variables • reading a volatile variable <acquire (curr,'x‘); r := x;> • writing a volatile variable x <x := e; release(curr,'x')>

  44. 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') >

  45. Locks • Lock the lock variable lck <acquire(curr,'lck'); lck.lock;> • Unlock lck <lck.unlock; release(curr,'lck')>

  46. 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')}

  47. Proof rules {norace('t1','y') /\ ('t0' = 't1' \/ 'x' ≠ 'y')} invalidat('t0','x') {norace('t1','y')}

  48. Recall example • Initially: • int x = 0; • boolean done = false • Thread 1: • int r; • … • while (!done){/*busywait*/} • r := x; • … Thread 0: … x := …. done := true; …

  49. 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

  50. Thread 1: r0 := done; assume !r0; assume false; [] assume r0; r := x Thread 1: int r; … while (!done){} r := x; …

More Related