At the root of several hazards • Example 1: 3 @volatile var x = 0 def m(): Unit = { Future { x = 1 } Future { x = 2 } .. // does not access x } What’s the value of x when an invocation of m returns?
always a problem! • Example 2: 4 import scala.collection.concurrent.TrieMap val set = new TrieMap[Int, Int] Future { set.put(1, 0) } set.put(2, 0) Eventually, set contains both 1 and 2, always
always a problem! • Example 2: 4 import scala.collection.concurrent.TrieMap val set = new TrieMap[Int, Int] Future { set.put(1, 0) } set.put(2, 0) Eventually, set contains both 1 and 2, always Bottom line: it depends on the datatype
its operations! • Example 3: 5 val set = new TrieMap[Int, Int] Future { set.put(1, 0) } Future { if (set.contains(1)) { .. } } set.put(2, 0) Result depends on schedule!
Q1: How do we know we have written a deterministic concurrent program? – What about Heisenbugs? • No race in 1’000’000 runs, race in run 1’000’001 6 Goal: simple criteria that guarantee determinism
Extend a future-style programming model with: – Lattice-based datatypes – Quiescence – Resolution of cyclic dependencies • Evaluation of expressivity and performance – Case study: static analysis of JVM bytecode 8 Crucial for determinism! Increases expressivity!
Programming model based on two core abstractions: cells and cell completers – Cell = shared “variable” – Cell[K,V]: read-only interface; read values of type V (akin to Future[V]) 9 Concurrently read and written
Programming model based on two core abstractions: cells and cell completers – Cell = shared “variable” – Cell[K,V]: read-only interface; read values of type V (akin to Future[V]) – CellCompleter[K,V] : write values of type V to its associated cell (akin to Promise[V]) 9 Concurrently read and written
Programming model based on two core abstractions: cells and cell completers – Cell = shared “variable” – Cell[K,V]: read-only interface; read values of type V (akin to Future[V]) – CellCompleter[K,V] : write values of type V to its associated cell (akin to Promise[V]) – V must have an instance of a lattice type class 9 Concurrently read and written
Programming model based on two core abstractions: cells and cell completers – Cell = shared “variable” – Cell[K,V]: read-only interface; read values of type V (akin to Future[V]) – CellCompleter[K,V] : write values of type V to its associated cell (akin to Promise[V]) – V must have an instance of a lattice type class 9 Monotonic updates Concurrently read and written
User IDs (code simplified) 12 implicit object IntSetLattice extends Lattice[Set[Int]] { val empty = Set() def join(left: Set[Int], right: Set[Int]) = left ++ right } // add a user ID userIDs.putNext(Set(theUserID)) val userIDs = CellCompleter[Set[Int]] Bounded join-semilattice
• Problem: when reading a cell’s value, how do we know this value is not going to change any more? – There may still be ongoing concurrent activities 13
• Problem: when reading a cell’s value, how do we know this value is not going to change any more? – There may still be ongoing concurrent activities – Manual synchronization (e.g., latches) error-prone 13
• Problem: when reading a cell’s value, how do we know this value is not going to change any more? – There may still be ongoing concurrent activities – Manual synchronization (e.g., latches) error-prone • Solution: 13
• Problem: when reading a cell’s value, how do we know this value is not going to change any more? – There may still be ongoing concurrent activities – Manual synchronization (e.g., latches) error-prone • Solution: 13 Koyaanisqatsi
• Problem: when reading a cell’s value, how do we know this value is not going to change any more? – There may still be ongoing concurrent activities – Manual synchronization (e.g., latches) error-prone • Solution: 13 Koyaanisqatsi
• Problem: when reading a cell’s value, how do we know this value is not going to change any more? – There may still be ongoing concurrent activities – Manual synchronization (e.g., latches) error-prone • Solution: 13 Quiescence
Intuitively: situation when values of cells are guaranteed not to change any more • Technically: – No concurrent activities ongoing or scheduled which could change values of cells 14
Intuitively: situation when values of cells are guaranteed not to change any more • Technically: – No concurrent activities ongoing or scheduled which could change values of cells – Detected by the underlying thread pool 14
Example 15 // add a user ID userIDs.putNext(Set(theUserID)) .. val pool = new HandlerPool val userIDs = CellCompleter[Set[Int]](pool) // register handler // upon quiescence: read result value of cell pool.onQuiescent(userIDs.cell) { collectedIDs => .. }
Example 15 // add a user ID userIDs.putNext(Set(theUserID)) .. val pool = new HandlerPool val userIDs = CellCompleter[Set[Int]](pool) // register handler // upon quiescence: read result value of cell pool.onQuiescent(userIDs.cell) { collectedIDs => .. } Safe to read from cell when pool quiescent!
Dataflow • Cells provide non-blocking interface – Low level: callbacks – High level: combinators • Important purpose: express concurrent dataflow 17 Like futures fut fun fut’ pred fut’’ fut.map(fun).filter(pred) Futures
Dataflow • Cells provide non-blocking interface – Low level: callbacks – High level: combinators • Important purpose: express concurrent dataflow • Problem: 17 Like futures fut fun fut’ pred fut’’ fut.map(fun).filter(pred) Futures
Dataflow • Cells provide non-blocking interface – Low level: callbacks – High level: combinators • Important purpose: express concurrent dataflow • Problem: 17 What if the dataflow graph is cyclic? Like futures fut fun fut’ pred fut’’ fut.map(fun).filter(pred) Futures
Example: • Static analysis of JVM bytecode • Task: Determine purity of methods • Rules: – A method is impure if it accesses a non-final field – A method is impure if it calls an impure method 18
Example: • Static analysis of JVM bytecode • Task: Determine purity of methods • Rules: – A method is impure if it accesses a non-final field – A method is impure if it calls an impure method – … 18
Example: • Static analysis of JVM bytecode • Task: Determine purity of methods • Rules: – A method is impure if it accesses a non-final field – A method is impure if it calls an impure method – … 18 A
Example: • Static analysis of JVM bytecode • Task: Determine purity of methods • Rules: – A method is impure if it accesses a non-final field – A method is impure if it calls an impure method – … 18 A B
Example: • Static analysis of JVM bytecode • Task: Determine purity of methods • Rules: – A method is impure if it accesses a non-final field – A method is impure if it calls an impure method – … 18 A B “calls”
Example: • Static analysis of JVM bytecode • Task: Determine purity of methods • Rules: – A method is impure if it accesses a non-final field – A method is impure if it calls an impure method – … 18 A B C “calls”
Example: • Static analysis of JVM bytecode • Task: Determine purity of methods • Rules: – A method is impure if it accesses a non-final field – A method is impure if it calls an impure method – … 18 A B C “calls”
• What if upon quiescence cells are empty and there is a dependency cycle? • Idea: Pluggable resolution policies • Example: Purity analysis should resolve all cells in cycle to “pure”, since could not show impurity 19 A B C
Policies • Role of the first type parameter of Cell[K,V] • Example: 20 object PurityKey extends Key[Purity] { def resolve[K <: Key[Purity]](cells: Seq[Cell[K, Purity]]) = cells.map(cell => (cell, Pure)) .. } resolve all cells to Pure
Conclusion • New concurrent programming model – Lattices and quiescence for determinism – Resolution of cyclic dependencies • Promising experimental results • Future work: – Optimization – Determinism as a statically-checked effect 28
Conclusion • New concurrent programming model – Lattices and quiescence for determinism – Resolution of cyclic dependencies • Promising experimental results • Future work: – Optimization – Determinism as a statically-checked effect 28 https://github.com/phaller/reactive-async