Isolating Determinism in Multi-threaded Programs

advertisement
Isolating Determinism in Multi-threaded
Programs
Lukasz Ziarek, Armand Navabi, Siddharth Tiwary, and Suresh Jagannathan
Purdue University
{lziarek, anavabi, stiwary, suresh}@cs.purdue.edu
Abstract. Futures are a program abstraction that express a simple yet
expressive form of fork-join parallelism. The expression future (e) declares that e can be evaluated concurrently with the future’s continuation. The expression touch (p) where p is a placeholder returned by
evaluating a future, blocks until the result of evaluating e is known.
Safe futures provide additional deterministic guarantees on the concurrent execution of the future with its continuation, ensuring that all data
dependencies found in the original (non-future annotated) version are respected. Earlier work on safe futures and related speculative abstractions
have considered their semantics in the context of an otherwise sequential
program. When safe futures are integrated within an explicitly concurrent program (e.g., to extract additional concurrency from within a sequential thread of control), the safety conditions necessary to guarantee
determinacy of their computations are substantially more complex. In
this paper, we present a dynamic analysis for enforcing determinism of
safe futures in an ML-like language with dynamic thread creation and
first-class references. Our analysis tracks the interaction between futures
(and their continuations) with other explicitly defined threads of control, and enforces an isolation property that prevents the effects of a
continuation from being witnessed by its future, even indirectly through
their interactions with other threads. Our analysis is defined via a novel
lightweight capability-based dependence tracking mechanism that serves
as a compact representation of an effect history. Implementation results
support our contention that the cost of enforcing selective isolation of
this kind is modest.
1
Introduction
A future is a program construct used to introduce simple fork-join parallelism
into sequential programs. The expression future(e ) evaluates e in a separate
thread of control that executes concurrently with its continuation, and immediately yields a placeholder (call it P ), a container created to hold e’s result. The
continuation can synchronize with its future by evaluating touch P . Evaluating
touch causes the continuation to block until the future completes, providing a
return value that gets stored in P.
A safe future imposes additional constraints on the execution of a future
and its continuation to preserve sequential semantics. By doing so, it provides a
simple-to-understand mechanism that provides deterministic parallelism, transforming a sequential program into a safe concurrent one, without requiring any
code restructuring. The definition of these constraints ensures that (a) the effects
of a continuation are never witnessed by its future, and (b) a read of a reference
r performed by a continuation is obligated to witness the last write to r made
by the future.
In the absence of side-effects, a program decorated with safe futures behaves
identically to a program in which all such annotations are erased, assuming all
future-encapsulated expressions terminate. In the presence of side-effects, however, unconstrained interleaving between a future and its continuation can lead
to undesirable racy behavior. The conditions described above which prevent such
behavior can be implemented either statically through the insertion of synchronization barriers (1; 2), or dynamically by tracking dependencies (3), treating
continuations as potentially speculative computations, aborting and rolling them
back when a dependence violation is detected.
Earlier work on safe futures considered their integration into sequential programs. In this context, the necessary dependence analysis has only to ensure that
the effects of concurrent execution adhere to the original sequential semantics. As
programs and libraries migrate to multicore environments, it is likely that computations from which we can extract deterministic parallelism are already part
of multi-threaded computations, or interact with libraries and/or other program
components that are themselves explicitly multi-threaded.
When safe futures are integrated within an explicitly concurrent program
(e.g., to extract additional concurrency from within a sequential thread of control), the necessary safety conditions are substantially more complex than those
used to guarantee determinacy in the context of otherwise sequential code. This is
because the interaction of explicitly concurrent threads among one another may
indirectly induce behavior that violates a safe future’s safety guarantees. For example, a continuation of a future F created within the context of one thread may
perform an effectful action that is witnessed by another thread whose resulting
behavior affects F ’s execution, as depicted in the following program fragment:
let val x = ref 0
val y = ref 0
in spawn( ... future (... !y ...);
x := 1);
spawn(... if !x = 1 then y := 1 ...)
end
In the absence of the future annotation, the dereference of y would always
yield 0, regardless of the interaction between the future’s continuation (here, the
assignment x := 1) and the second thread.
In this paper, we present a dynamic program analysis that tracks interactions
between threads, futures, and their continuations that prevents the effects of continuations from being witnessed by their futures through cross-thread dataflow,
as described in the example above. Our technique allows seamless integration
of safe futures into multi-threaded programs, and provides a mechanism that
enables extraction of additional parallelism from multi-threaded code without
requiring any further code restructuring, or programmer-specified synchronization. To the best of our knowledge, ours is the first analysis to provide lightweight
thread-aware dependence tracking for effect isolation in the context of a language
equipped with dynamic thread creation, first-class references, to support deterministic parallelism.
Our contributions are as follows:
1. We present a dynamic analysis that isolates the effects of a continuation C
from its future F even in the presence of multiple explicit threads of control.
The isolation property is selective, allowing C to interact freely with other
threads provided that the effects of such interactions do not eventually leak
back to F .
2. We introduce capabilities, a new dependence analysis structure, that enables
lightweight dynamic tracking of effects, suitable for identifying dependence
violations between futures and their continuations. The tracking mechanism
facilitates implementations that allow rollback of speculative computations
(i.e., those actions that witness effects of a future’s continuation before the
future has completed) that contribute to violations of the safety conditions
that define a deterministic parallel computation.
3. We describe an implementation in MLton, a whole-program optimizing compiler for Standard ML, that shows the overhead of dynamic dependence
tracking for monitoring this kind of isolation, and remedying violations, is
relatively small (around 15% even under conditions that induce substantial
interactions among futures and threads).
The rest of the paper is organized as follows. In Sec. 2 we provide examples
that illustrate the issues that arise in enforcing deterministic parallelism in the
presence of explicit threads of control. In Sec. 3 and 4, we introduce capabilities
and define them formally. Implementation details and benchmark results are
provided in Sec. 5. We provide comparisons to previous work in Sec. 6 and
conclude in Sec. 7.
2
Safe Futures and Threads
Safe futures are intended to ensure deterministic parallelism; they do so by guaranteeing the following two safety properties: (1) a future will never witness its
continuation’s effects and (2) a continuation will never witness the future’s intermediate effects. In a sequential setting, the second condition implies a continuation must witness the logically last effects of a future.
To illustrate these properties, consider the two example programs given in
Fig. 1. The code that is executed by the future is highlighted in gray. When
a future is created it immediately returns a placeholder, which when touched
will produce the return value of the future. A touch is a synchronization point,
guaranteeing that the future has completed.
In the program on the left, the future writes 1 to the shared variable x. The
continuation (the code following in) reads from x. To ensure that the behavior
Initially: x=0, y=0
Program 1
let fun f() = x := 1
val tag = future (f)
in !x ; touch (tag)
end
Program 2
let fun f() = !y
val tag = future (f)
in y := 1 ; touch (tag)
end
Fig. 1. Two programs depicting the safety properties that must be enforced to achieve
deterministic parallelism for sequential programs annotated with futures.
of this program is consistent with a sequential execution, the continuation is only
allowed to read 1. In the program on the right, the future reads from the shared
variable y while the continuation writes to y. The future cannot witness any
effects from the continuation, and therefore can only read 0 for y. To correctly
execute the futures in programs 1 and 2 concurrently with their continuations
we must ensure that the future is isolated from the effects of the continuation
and that the future’s final effects are propagated to the continuation.
2.1
Interaction with Threads
The creation of new threads by either the future or the continuation requires reasoning about the created threads’ actions. If a future creates a thread of control,
that thread’s reads and writes may need to be isolated from the continuation’s
effects if its actions can be later witnessed by the future. We need to ensure
that if a future witness’s a write performed by the thread it created, the future’s
continuation cannot witness any prior writes. On the other hand, a future must
be isolated from the effects of any threads that a continuation creates. These
threads are created logically after the future completes and therefore the future
cannot witness any of their effects; if it did, the guarantee that its concurrent
execution with its continuation is observably equivalent to a sequential evaluation would be compromised. To illustrate, consider the two examples programs
given in Fig. 2.
In program 1 the future creates a thread which writes to the shared variable
x. The future then branches on the contents of x, and if x contains the value 1
it writes 2 to y. The continuation reads from both x and y. The continuation
may read either 0 or 1 from x and may read either 0 or 2 from y depending on
the interleaving between the future and the thread executing g. However, the
continuation cannot read 2 from y and also read 0 from x; such behavior would
imply the continuation executes after the created thread, but prior to the branch
in the future which would violate our desired safety property.
In program 2 the continuation creates a thread which writes to the shared
variable z. Since the thread that the continuation creates should logically execute
after the completion of the future, the future can only see the value 0 for z.
Initially: x=0, y=0, z=0
Program 1
Program 2
let fun g() = x := 1
let fun h() = z := 2
fun f() = spawn (g)
fun f() = !z
if !x = 1
val tag = future (f)
then y := 2 in z := 1; spawn(h);
val tag = future (f)
touch (tag)
in !x ; !y; touch (tag)
end
end
Fig. 2. Two programs depicting the safety properties that must be enforced to achieve
deterministic parallelism for futures and continuations which create new threads of
control.
2.2
Transitive Effects
In the presence of multiple threads of control a future may incorrectly witness a
continuation’s effects transitively. Consider the sample program given in Fig. 3
that consists of two threads of control and one future denoted by the gray box.
Notice that the future and its continuation do not access the same memory
locations. The future simply reads from y and the continuation writes to x. Can
the future read the value 2 from the shared variable y? Reading the value 2 for
y would be erroneous because Thread 2 writes the value 2 to y only if x contains
the value 1, implying the continuation’s effects were visible to its future.
Initially: x=0, y=0, z=0
Thread 1
Thread 2
let fun f() = !y
val tag = future (f)
in x := 1; touch (tag)
end
if x = 1
then y := 2
else y := 3
Can the future see y = 2?
Fig. 3. Although the future and continuation do not conflict in the variables they
access, the future, by witnessing the effects of Thread 2, transitively witnesses the
effects of the continuation.
Similarly, a continuation may incorrectly witness a future’s intermediate effects transitively (see Fig. 4). Here, functions g, h, and i force a particular
interleaving between the future and Thread 2. It is incorrect for the continuation to witness the write of 1 to z by Thread 2 because Thread 2 subsequently
overwrites z and synchronizes with the future by writing to y and then waiting
until the future writes 2 to x. Thus, the continuation should only witness the
value 2 for z.
Initially: x=0, y=0, z=0
Thread 1
let fun g() = if !y = 2
then ()
else g()
fun f() = x := 1;g();
x := 2
val tag = future (f)
in !z; touch (tag)
end
Thread 2
let fun h() = if !x = 1
then z := 1
else h()
fun i() = if !x = 2
then ()
else i()
in h(); z:= 2; y := 2; i()
end
Can the continuation see z = 1?
Fig. 4. Although the future and continuation do not conflict in the variables they access, the continuation may incorrectly witness the future’s intermediate effects through
updates performed by Thread 2.
As another example, consider the sample program given in Fig. 5 consisting
of two explicitly created threads and one future denoted by the gray box. The
functions g() and h() encode simple barriers that ensure synchronization between the future and Thread 2. Is it possible for the continuation to read the
value 1 for the shared variable z? Notice that the future does perform a write
of 1 to z and in fact this is the last write the future performs to that shared
variable. However, Thread 2 assigns 2 to z prior to assigning 2 to y. The future
in Thread 1 waits, by executing the function g, until Thread 2 writes 2 to y.
Therefore, the continuation must witness the write of 2 to z as this shared update logically occurs prior to the completion of the future. The future’s write
of 1 to z is guaranteed to occur prior to the write of 2 to z in Thread 2, since
Thread 2 waits until the write to x to perform its update to z.
Initially: x=0, y=0, z=0
Thread 1
let fun g() = if !y = 2
then ()
else g()
fun f() = z := 1;
x := 1; g()
val tag = future (f)
in !z; touch (tag)
end
Thread 2
let fun h() = if !x = 1
then ()
else h()
in h(); z := 2; y := !z
end
Can the continuation see z = 1?
Fig. 5. The continuation cannot witness the future’s write to z as this is an intermediate effect. The write to z in Thread 2 is transitively made visible to the continuation
since the future synchronizes with Thread 2.
2.3
Future and future interaction
Similar issues arise when two futures witness each other’s effects (see Fig. 6).
Here, each thread creates a future; there are no dependencies between the future
and its continuation. Assuming no compiler reorderings, the continuation of the
future created by Thread 1 writes to x and the continuation of the future created
by Thread 2 writes to y. The futures created by Thread 1 and 2 read from y
and x respectively. It should be impossible for Thread 1 to read 1 from y and
Thread 2 to read 1 from x. However, executing the futures arbitrarily allows for
such an ordering to occur if the continuation of the future created by Thread
2 executes prior to the future in Thread 1 and the continuation of that future
executes prior to the future created by Thread 2. In such an execution the futures
witness values that logically should occur after their executions.
Initially: x=0, y=0, z=0
Thread 1
let fun f() = !y;
val tag = future (f)
in x := 1; touch (tag)
end
Thread 2
let fun g() = !x;
val tag = future (g)
in y := 1; touch (tag)
end
Can the future in Thread 1 see y = 1 and the future
in Thread 2 x = 1?
Fig. 6. Two futures created by separate threads of control may interact in a way that
violates sequential consistency even though each future itself contains no violations
with its continuation.
3
Capabilities
To enable scalable construction of safe futures, we formulate a strategy that associates capabilities with threads, futures, as well as the locations they modify
and read. Abstractly, capabilities are used to indicate which effects have been
witnessed or manifested by a future and its continuation either directly or indirectly as well as to constrain which locations a future and its continuation may
read or modify.
A capability is defined as a mapping between labels ` 1 , denoting the dynamic
instances of a future, and tags. We introduce three conceptual tags: F to denote
a thread or location has been influenced by a future, C to denote a thread or
location has been influenced by a continuation, and FC for a thread or location
that first was influenced by a future and later by its continuation. Observe that
it is illegal for a computation to witness the effects of a continuation and then the
1
We can think of ` as a unique identifier for each dynamic instance of a future.
continuation’s future. Tracking multiple labels allows us to differentiate between
effects of different futures.
When a future ` is created, a constraint is established that relates the execution of the thread executing this future with its continuation. Namely, we
add a mapping from ` to F for the thread executing the future and a mapping
from ` to C for the thread executing the continuation. When the future or continuation reads or writes from a given location, we propagate its constraints to
that location. Therefore, capabilities provide a tainting property that succinctly
records the history of actions performed by a thread and which threads as well
as locations those actions have influenced.
To ensure a given thread T ’s read or write is correct, it must be the case
that either (a) T has thus far only read values written by the future `; (b) T
has thus far only witness values written by the continuation of future `; or (c)
T had previously read values written by the future, but now only reads values
written by the futures’ continuation. If T has previously read values written
by the future `, and then subsequently read values written by its continuation;
allowing T to read further values written by the future ` would break correctness
guarantees on `’s execution. Thus, prior to a read or a write to a given location, if
that location has capabilities associated with it, we must ensure that the thread
which is trying to read or write from that location also has the same capabilities.
Capabilities can be lifted in the obvious manner. A capability can be lifted to
a new capability that is more constrained. A thread or location with no capability
for a given label ` can lift to either C or F. A thread which has a capability of F
can lift the capability to FC and similarly a thread with a capability C can lift the
capability to FC. A future and its continuation can never lift their capabilities
for their own label. We allow a given thread to read or write to a location if its
capabilities are equal to those for the given location. When a future completes,
it is safe to discard all capabilities related to the future.
3.1
Examples
Capabilities can be utilized to prevent a future from witnessing the transitive
effects from its continuation. As an example, see the program trace for the code
given in Fig. 3 depicted in Fig. 7. When the future is created a new thread of
control is also created. The future receives a capability ` 7→ F and its continuation ` 7→ C. When the continuation performs the write to x, we propagate its
constraints to the x. When Thread 2 reads from x, it acquires the constraints
associated with x. These constraints get further propagated with the write to y
in Thread 2. Now, when the future attempts to read from y we detect the error
as the future has a mapping of ` 7→ F and the location has a mapping of ` 7→ C.
The future cannot lift its capability to anything other than F, and we cannot lift
y’s capability to CF.
Based on capability mappings, we can distinguish between speculative and
non-speculative computations. A continuation of a future is a speculative computation until the future completes. Similarly, any thread which communicates with
a continuation of a future, becomes speculative at the communication point. On
Thread 1
Thread 2
future
l -> C
Memory
x = 0
y = 0
l -> F
x := 1
Thread 1
Thread 2
future
l -> C
l -> C
!x
y := 2
Memory
x = 1
y = 0
l -> C
l -> F
x := 1
Thread 1
Thread 2
future
l -> C
l -> C
!x
y := 2
Memory
x = 1
y = 2
l -> C
l -> C
l -> F
x := 1
!y
Fig. 7. Capabilities can be utilized to ensure a future does not witness transitive effects
from a continuation. This example show capabilities for the code given in Fig. 3.
the other hand, any thread which has only F capabilities or an empty capability
map is non-speculative.
let fun g() = ...
fun h() = ... future(g) ...
fun i() = ... future(h) ...
in i()
end
Fig. 8. A code example that creates nested futures.
A future may in turn be speculative. As an example, consider the program
given in Fig. 8 which creates nested futures. The function i creates a future
which evaluates h. The function h in turn creates a new future to evaluate g.
At the point of the creation of the future to evaluate g, the remainder of the
function h (the future evaluating g’s continuation) is speculative. The thread
evaluating g as a future would have capabilities `g 7→ F and `h 7→ F and the
thread evaluating the continuation would have capabilities `g 7→ C and `h 7→ F.
Thread 2
Thread 1
future
l 1 -> C
l 1 -> F
x := 1
Memory
future
x = 0
y = 0
l 2 -> C
l 2 -> F
y := 1
Memory
Thread 2
Thread 1
future
l 1 -> F
l 1 -> C
x := 1
x = 1
y = 1
future
l 2 -> C
y := 1
!x
future
l 2 -> F
Memory
Thread 2
Thread 1
x = 1
future
y = 1
l 1 -> C
l 2 -> F x := 1
l 1 -> F
l 2 -> C !y
l 1 -> C
l 2 -> C
l 2 -> C
y := 1
!x
l1
l2
l2
l1
->
->
->
->
C
F
C
F
l 2 -> F
l 1 -> C
Fig. 9. Capabilities can be utilized to ensure consistency between separate futures
created from distinct threads of control and their continuations. This example show
capabilities for the code given in Fig. 6.
3.2
Handling future to future interactions
We further extend our notion of capabilities to handle future to future interactions by ensuring that the future and its continuation have consistent capabilities
upon the futures completion. Since multiple threads of control can create futures
(as in our example in Fig. 6) it is possible for a continuation of a future f to
witness the effects of some other future g while the future f witnesses the effects
of g’s continuation. This would violate the dependencies imposed by sequential
evaluation of both threads of control. To account for this, we check that a future and its continuation have consistent capabilities when a future completes.
In addition, when a location or thread that has a speculative capability (i.e. C,
FC) acquires a capability for a future ` (i.e. ` 7→ F) we impose an ordering constraint between the future of the speculative capability and the future `. Namely,
since the thread or location witnessed some future’s continuation’s effect, the future ` logically must occur after the future of the continuation whose effect was
witnessed.
As an example, consider the program trace for the code in Fig. 6 given
in Fig. 9. Thread 1 creates a futures with label `1 and Thread 2 creates a
futures with label `2 . Both continuations do writes, the continuation of `1 to x
and the continuation of `2 to y. The locations x and y afterwards contain the
capabilities `1 7→ C and `2 7→ C respectively. Future `2 reads from x and acquires
the capability `1 7→ C, it propagates its capability `2 7→ F to the location x
and the continuation of `1 . Similarly, future `1 reads from y and acquires the
capability `2 7→ C, it propagates its capability `1 7→ F to the location y and
the continuation of `2 . Now if either of the futures completes, it must check that
it has consistent capabilities with its continuation. Notice that future `1 has a
capability `2 7→ C but its continuation has a capability `2 7→ F. The capabilities
are not consistent indicating that there is a violation of dependencies imposed
by a sequential evaluation of both threads of control.
4
Semantics
Our semantics is defined in terms of a core call-by-value functional language with
threading and references. The language includes primitives for creating threads
( spawn), creating shared references ( ref), reading from a shared reference (!), and
assignment to a shared reference (:=). We extend this core language with primitives to construct futures and to wait on their completion (see Fig. 10). Notably,
our language omits locking primitives. Notice, locks simply ensure that multiple
updates to shared locations occur without interleavings from other threads. The
visibility dependencies induced by the acquisition and release of locks occur only
if the threads acquiring and releasing the same lock read and write to the same
location. Therefore, it is enough to track reads and updates to shared locations.
As such, locks do not add any interesting semantic issues and are omitted for
brevity.
id ∈ ID := TID + location
ι ∈ Tag := · | F̂ | Ĉ | F | C | FC | 2
v ∈ Value
` ∈ Label
t ∈ TID
l ∈ Location
fin
γ ∈ FutureMap := Label → Value
fin
∆ ∈ LocalCapabilityMap := Label → Tag + φ
fin
δ ∈ CapabilityMap := ID → LocalCapabilityMap
fin
σ ∈ Store := Location → TID × Value
I ∈ FutureIdentifier := Label + φ
e :=
|
|
|
unit | x | v | λx.e | e e
spawn e | ref e
e := e | !e
future e | touch e
E := · | E e | v E
| E := e | l := E | ref E
| !E | touch E
v := unit | l | ` | λx.e
T ∈ Thread := (tI , e)
T := T | T || T
Fig. 10. Language Syntax and Grammar
Tag Enrichment
Capability Lifting
· F · C
F FC C FC
lift ` ι δ id = δ[id 7→ δ(id)[` 7→ ι]]
δ id ` = · if ` ∈
/ dom(δ(id))
Capability Lifting No Ordering
(δ id `) ι
δ 0 = lift ` ι δ id
ι ∈ {C, FC}
0
γ, δ, σ, T → γ, δ , σ, T
Capability Lifting With Ordering
(δ id `) ι
δ 0 = lift ` ι δ id
ι ∈ {F}
id, `, δ 0 , T ⇒ δ 00
γ, δ, σ, T → γ, δ 00 , σ, T
Clear Capability
δ 0 = lift ` 2 δ id
γ(`) = v
γ, δ, σ, T → γ, δ 0 , σ, T
Local Capability Map Equivalence
dom(∆1 ) = dom(∆2 )
∀ ` ∈ dom(∆1 ) ∆1 (`) ≈ ∆2 (`)
ι1 = ι2
∆1 3∆2
ι1 ≈ ι2
F̂ ≈ F
Ĉ ≈ C
Ĉ ≈ FC
Ordering Constraints
δ 0 = fold (lift ` FC δ, {t | t ∈ T , ∃ `0 ∈ {`00 | δ id `00 ∈ {C, FC, Ĉ}} δ t `0 = Ĉ, (δ t `) = C})
δ 00 = fold (lift ` F δ 0 , {t | t ∈ T , ∃ `0 ∈ {`00 | δ 0 id `00 ∈ {C, FC, Ĉ}} δ 0 t `0 = Ĉ, (δ 0 t `) =· })
id, `, δ, T ⇒ δ 00
Consistency Check
dom(∆1 ) = dom(∆2 )
∀ ` ∈ dom(∆1 ) ∆1 (`) 1 ∆2 (`)
∆ 1 < ∆2
F1F
C1C
F 1 FC
FC 1 C
Fig. 11. Rules for tag enrichment (), capability lifting (lift), and capability map
equivalence (3).
In our syntax, v ranges over values, l over locations, e over expressions, x
over variables, ` over future identifiers (labels), and t over thread identifiers. A
program state is defined as quadruple consisting of a future map (γ), a capability
map (δ), a store (σ), and a set of threads (T ). A future map (γ) maps future
identifiers ` to values or the distinguished “empty” value φ. When a future is
created, the map is extended to map the future identifier to φ and once the future
App
γ, δ, σ, (tI , E[(λx.e) v]) || T → γ, δ, σ, (tI , E[e[v/x]]) || T
Ref
l fresh
I
γ, δ, σ, (t , E[ref v]) || T → γ, δ[l 7→ δ(t)], σ[l 7→ (t, v)], (tI , E[l]) || T
Touch
γ(`) = v
γ, δ, σ, (tI , E[touch `]) || T → γ, δ, σ, (tI , E[v]) || T
Spawn
t0 fresh
γ, δ, σ, (tI , E[spawn e]) || T → γ, δ[t0 7→ δ(t)], σ, (t0φ , e) || (tI , E[unit]) || T
Future
t0 fresh ` fresh
δ = lift ` F̂ δ[t0 7→ δ(t)] t δ 00 = lift `0 Ĉ δ 0 t0
0
0
γ, δ, σ, (tI , E[future e]) || T → γ[` 7→ φ], δ 00 , σ, (t` , e) || (t0I , E[`]) || T
End Future
∀t0 s.t. δ t0 ` = Ĉ :
δ(t) = ∆t [` 7→ F̂]
δ(t0 ) = ∆t0 [` 7→ Ĉ] ∆t < ∆t0
γ, δ, σ, (t` , v) || T → γ[` 7→ v], δ[t 7→ φ], σ, (tφ , v) || T
Read
δ(t)3δ(l)
σ(l) = (t0 , v)
γ, δ, σ, (tI , E[! l]) || T → γ, δ, σ, (tI , E[v]) || T
Read Local
σ(l) = (t, v)
γ, δ, σ, (tI , E[! l]) || T → γ, δ, σ, (tI , E[v]) || T
Write
δ(t)3δ(l)
I
γ, δ, σ, (t , E[l := v]) || T → γ, δ, σ[l 7→ (t, v)], (tI , E[unit]) || T
Fig. 12. Evaluation rules.
has completed the map will contain the return value of the future. A capability
map relates identifiers (either a location or a thread id) to a local capability map
(∆). A local capability map maps future identifiers to tags (ι). We use the short
hand δ id ` for ((δ id) `). A store is a mapping between a location and a pair
consisting of a thread identifier and a value. The thread identifier specifies the
last thread to change this location. We use the following short hand: t ∈ T
to signify that a thread with a thread identifier t is a part of the thread group
T . Evaluation is specified via relation (→). Evaluation rules are applied up to
commutativity of parallel composition (k).
We define capabilities over tags and labels as well as rules for capability
lifting and tag enrichment in Fig. 11. The semantics makes use of a relation (see Tag Enrichment) that captures the obvious ordering relationship among
tags. Informally, tag ι can be enriched to tag ι0 if ι imposes the same or fewer
restrictions than ι0 . Note the presence of two additional tags F̂ and Ĉ that we
haven’t discussed thus far – these tags are used to build capabilities for threads
executing the future and its continuation and neither can be enriched. If thread t
executes a future expression with label `, its capability map is extended with the
capability ` 7→ F̂. Similarly, the capability map of the thread created to execute
the continuation is updated with the capability ` 7→ Ĉ. In addition, we provide
a tag to indicate that a future has completed (2). If a local capability map does
not contain a binding for a given label, then by definition that label maps to
empty (·).
Rule Capability Lifting introduces capabilities to a thread’s or location’s
capability map. If the thread does not already have a tag for this future, it can
choose one (either F or C). The thread’s capability map is then updated (via
auxiliary function lift) to reflect this new binding. If the thread chooses F, it will
allow the thread to communicate to the future. Conversely, if the thread chooses
C, it will allow the thread to communicate with the continuation. Similarly lifting
to FC will allow the thread to continue to communicate with the continuation.
The same holds for locations.
There are two rules that allow threads and locations to lift their capabilities.
Both rules can be applied at any evaluation step. This allows for simple equivalence checks between capability maps when threads read and write to shared
locations. Rule Capability Lifting No Ordering allows threads and locations to lift to capabilities with tags C or FC. Rule Capability Lifting With
Ordering, which allows lifting to F, imposes an additional ordering constraint.
If we lift a capability map to contain the binding ` to F that also contains bindings for some future `0 to C, FC, or Ĉ this imposes an ordering constraint between
the future ` and the future `0 , for which this thread has witnessed its continuation’s effects. Namely, the future ` must executed after the future `0 (see the
example in Fig. 9). Ordering constrains are added by the rule Ordering Constraints which for a given capability map ∆, an identifier id, and a future `
simply adds a mapping of ` to F for all threads which are continuations and that
have mappings of C, FC, or Ĉ in ∆. Notice, this additional constraints prevents
the futures for those continuations from witnessing the effects of the continua-
tion of ` (see Rule End Future). We do not need to add this constraint if the
continuation already has a binding to F or FC for ` as it is subsumed by these
bindings. If the continuation has a binding of ` to C, then we add an FC binding
to impose the ordering constraint.
Further evaluation rules are given in Fig. 12. The rule for function application
is standard. When a shared memory location is created (see rule Ref), we extend
the capability mapping with the location and the local capability map for the
thread which created the reference. When a future is claimed (see rule Touch),
the return value for the future (stored in the future map γ) is installed for
the continuation. When a new thread of control is created (see rule Spawn) it
inherits the local capability map of its parent. When a new future is created we
create a new thread of control to evaluate the continuation. Similarly to spawn,
the newly created thread inherits its parents local capability map. The thread
evaluating the future is tag with the future’s label and its local capability map
is extended with a mapping from that label to F̂. The thread created to evaluate
the continuation has its capability map extend with a mapping from the label
to Ĉ. When a future has completed (see rule End Future), its return value is
captured in γ and its local capability map is checked for equivalence against the
local capability map of its continuation as well as any threads created by the
continuation (all such threads will have a capability of Ĉ for the future identifier).
We disregard the F̂ and Ĉ tags for the label representing the future itself.
There are two read rules – one for reading values written by the thread
performing the read, and the other for reading values written by other threads
of control. Thread local reads require no consistency checks. However, during
a read of a value written by another thread, the capability maps of the thread
performing the read and the location must be equivalent. During a write to
memory, the rule Write asserts that the local capability map for the location
is equivalent to that of the thread performing the write.
4.1
Progress and Correctness
In the following, we write s̄ to define an evaluation sequence, a potentially infinite
trace of evaluation steps. If s̄ = s0 →∗ sn , we define init(s̄) as s0 and end(s̄) as
sn corresponding to the initial state and end state of the sequence s̄ respectively.
Additionally we use the following shorthand s ∈ s̄ if s̄ = s0 →∗ s →∗ sn to
denote the fact that state s occurs in the evaluation sequence s̄. We now define
the notion of a non-speculative state.
Definition 1.[Non-speculative]: Given a state s = γ, δ, σ, T , N (s) holds if ∀ t ∈
T , ∀ ` δ t ` ∈ {· , F, F̂, Ĉ, 2} and ∀ l ∈ dom(σ) δ l ` ∈ {· , F, 2}
A non-speculative state is a state in which no effects from a continuation are
visible until its future has completed. Continuations and the threads they create are permitted to do pure computation, but cannot read or write to shared
memory.
Theorem 1. [Progress:] If N (s) holds, then either s is final or ∃ s0 s.t. s → s0 .
We define progress in terms of non-speculative states: given a non-speculative
state s, it is either final, where all threads have evaluated to values, or there
exists a state s0 which can be derived from s. The proof of progress follows from
induction on the length of evaluation sequences and case analysis of the semantic
rules.
Definition 2. [Safe Evaluation Sequence:] Given an evaluation sequence s̄, let si =
0
γ, δ, σ, (tI , E[future e]) || T and sj = γ 0 , δ 0 , σ 0 , (t` , e) || (t0I , E[`]) || T and s̄ =
00
s0 →∗ si → sj →∗ sn , then S(s̄) holds if (γ 00 , δ 00 , σ 00 , (t` , v) || (t0I , E[`]) || T ) ∈
s̄.
We now define correctness over safe evaluation sequences. A safe evaluation sequence is an evaluation sequence s̄ where all continuations are evaluated after
their future has completed.
Theorem 2. [Correctness:] If s0 →∗ γ, δ, σ, (tφ1 , e1 ) || ... || (tφn , en )
then ∃ s̄ s.t. S(s̄) and s0 = init(s̄) and (γ, δ, σ, (tφ1 , e1 ) || ... || (tφn , en )) = end(s̄)
For any evaluation sequence whose last state is a state in which all futures
have completed, there exists a safe evaluation sequence with the same start and
end state. To prove soundness, we show that we can permute such an evaluation
sequence into a safe evaluation sequence. We exploit the following permutation
lemma that shows that any consistent evaluation sequence can be permuted into
an ordered evaluation sequence. An ordered evaluation sequence is an evaluation
sequence where all continuations are evaluated after their futures or a sequence
which contains no futures.
Definition 3. [Ordered Evaluation Sequence:] Given an evaluation sequence s̄, let
0
si = γ, δ, σ, (tI , E[future e]) || T and sj = γ 0 , δ 0 , σ 0 , (t` , e) || (t0I , E[`]) || T and
00
000
sk = γ 00 , δ 00 , σ 00 , (t` , e0 ) || (t0I , E[`]) || T and sl = γ 000 , δ 000 , σ 000 , (t` , e0 ) || (t0I , e00 ) || T
0000
and s̄ = s0 →∗ si → sj →∗ sk → sl →∗ sn such that (γ 0000 , δ 0000 , σ 0000 , (t` , e0 ) || T )
= end(s̄) then O(s̄)
or
∀ s ∈ s̄, such that s = (γ, δ, σ, T ) and T = (tφ1 , e1 ) || ... || (tφn , en ) then O(s̄).
Notice that every safe sequence is an ordered sequence. Thus, if S(s̄) then
O(s̄). Similarly, any ordered sequence that has an end state in which all futures are completed is a safe evaluation sequence. Next we define a consistent
sequence. We first define a consistent state as a state in which all futures have
a consistent local capability map with their continuations (<).
Definition 4a. [Consistent State:] Given s = (γ, δ, σ, T ) such that ∀ (t` , e) ∈ T
and ∀t0 s.t. δ t0 ` = Ĉ : δ(t) = ∆t [` 7→ F̂] δ(t0 ) = ∆t0 [` 7→ Ĉ] ∆t < ∆t0 then
C(s)).
We define a consistent sequence as any sequence which the consistency property (C) holds on all states. Notice, the evaluation rules only check for consistency
when a future has completed. However, any sequence for which the consistency
property does not hold at every state will not be able to apply the End Future
rule.
Definition 4b. [Consistent Sequence:] Given s̄, if for all s ∈ s̄ C(s) then C(s̄).
A sequence in which all futures have completed is by definition a consistent
sequence (see rule End Future). By definition of Theorem 2, we know all futures have completed in s̄ and thus C(s̄). Therefore, to prove correctness we must
show that all consistent evaluation sequences can be permuted into an ordered
evaluation sequence. We write s̄0 = P(s̄) to denote that the evaluation sequence
s̄0 is a permutation of evaluation sequence s̄.
Lemma 1. [Permutation:] ∀ s̄ s.t.C(s̄) then ∃ s̄0 s.t. O(s0 ) and s̄0 = P(s̄)
We first introduce two corollaries that define permutations on sequences of
length two. The first defines permutations of pure computation and the second
defines permutations on capability lifting.
Corollary 1a. [Permutation of Pure Computation:]
If
γ, δ, σ, (tI , e) || (t0I1 , e00 ) || T →
γ, δ, σ, (tI , e0 ) || (t0I1 , e00 ) || T →
γ, δ, σ, (tI , e0 ) || (t0I1 , e000 ) || T then
γ, δ, σ, (tI , e) || (t0I1 , e00 ) || T →
γ, δ, σ, (tI , e) || (t0I1 , e000 ) || T →
γ, δ, σ, (tI , e0 ) || (t0I1 , e000 ) || T
The corollary holds based on the definitions of App, Touch, Read, and Read
Local.
Corollary 1b. [Permutation of Pure Computation:]
If
γ, δ, σ, (tI , e) || (t0I1 , E[(λx.e) v]) || T →
0
γ 0 , δ 0 , σ 0 , (tI , e0 ) || (t0I1 , E[(λx.e) v]) || T →
0
γ 0 , δ 0 , σ 0 , (tI , e0 ) || (t0I1 , E[e[v/x]]) || T then
γ, δ, σ, (tI , e) || (t0I1 , E[(λx.e) v]) || T →
γ, δ, σ, (tI , e) || (t0I1 , E[e[v/x]]) || T →
0
γ 0 , δ 0 , σ 0 , (tI , e0 ) || (t0I1 , E[e[v/x]]) || T
The corollary holds based on the definition of App.
Corollary 2a. [Permutation of Capability Lifting:]
If
γ, δ, σ, T →
γ, δ[id 7→ δ(id)[` 7→ ι]], σ, T →
γ, δ[id 7→ δ(id)[` 7→ ι], id0 7→ δ(id0 )[`0 7→ ι0 ]], σ, T
then
γ, δ, σ, T →
γ, δ[id0 7→ δ(id0 )[`0 7→ ι0 ]], σ, T →
γ, δ[id 7→ δ(id)[` 7→ ι], id0 7→ δ(id0 )[`0 7→ ι0 ]], σ, T
The corollary holds based on the definitions of Capability Lifting No Ordering, Capability Lifting With Ordering, and Clear Capability.
Corollary 2b. [Permutation of Capability Lifting:]
If
γ, δ, σ, T →
γ, δ[id 7→ δ(id)[` 7→ ι]], σ, T →
γ, δ[id 7→ δ(id)[` 7→ ι, `0 7→ ι0 ]], σ, T
then
γ, δ, σ, T →
γ, δ[id 7→ δ(id)[`0 7→ ι0 ]], σ, T →
γ, δ[id 7→ δ(id)[` 7→ ι, `0 7→ ι0 ]], σ, T
The corollary holds based on the definitions of Capability Lifting No Ordering, Capability Lifting With Ordering, and Clear Capability.
Corollary 3. [Ordering of Threads:]
If
0
00
γ, δ, σ, (tI , e) || T → γ 0 , δ 0 , σ 0 , (tI1 , e0 ) || T → γ 00 , δ 00 , σ 00 , (tI2 , e00 ) || T
then
0
00
O(γ, δ, σ, (tI , e) || T → γ 0 , δ 0 , σ 0 , (tI1 , e0 ) || T → γ 00 , δ 00 , σ 00 , (tI2 , e00 ) || T )
We now proceed to prove the base case of Lemma 1. We examine the cases
not covered by corollaries 1, 2, and 3.
Case Future:
Given s̄ =
γ, δ, σ, (tI , E[future e]) || (tI11 , e0 ) || T →
γ 0 , δ 0 , σ, (t` , e) || (t0I , E[`]) || (tI11 , e0 ) || T →
0
γ 00 , δ 00 , σ 0 , (t` , e) || (t0I , E[`]) || (tI11 , e00 ) || T
By definition of rule Future we know:
γ, δ, σ, (tI , E[future e]) || (tI11 , e0 ) || T →
0
γ 000 , δ 000 , σ 0 , (tI , E[future e]) || (tI11 , e0 ) || T →
0
γ 00 , δ 00 , σ 0 , (t` , e) || (t0I , E[`]) || (tI11 , e00 ) || T
Case Spawn:
Given s̄ =
γ, δ, σ, (tI , E[spawn e]) || (tI11 , e0 ) || T →
I
γ, δ 0 , σ, (t0 , e) || (tI11 , e0 ) || (tI , E[unit]) || T →
0
I
γ 0 , δ 00 , σ 0 , (t0 , e) || (tI11 , e00 ) || (tI , E[unit]) || T
By definition of Spawn we know:
γ, δ, σ, (tI , E[spawn e]) || (tI11 , e0 ) || T →
0
γ 0 , δ 000 , σ 0 , (tI , E[spawn e]) || (tI11 , e00 ) || T →
0
I
γ 0 , δ 00 , σ 0 , (t0 , e) || (tI11 , e00 ) || (tI , E[unit]) || T
Case End Future:
Given s̄ =
γ, δ, σ, (tφ , E[future v]) || T →
0
γ, δ 0 , σ 0 , (t0φ , E[`]) || (t` , v) || T
0
γ[` 7→ v], δ 0 [t 7→ φ], σ 0 , (t0φ , E[`]) || (tφ , v) || T
We know O(s̄) by Corollary 3.
Case Write:
Given s̄ =
γ, δ, σ, (t0I1 , e) || (tI , E[l := v]) || T →
0
γ, δ 0 , σ 0 , (t0I1 , e0 ) || (tI , E[l := v]) || T →
0
γ, δ 0 , σ 0 [l 7→ (t, v)], (t0I1 , e0 ) || (tI , E[unit]) || T
If e =!l or e = (l := v 0 ) then we know O(s̄) otherwise
γ, δ, σ, (t0I1 , e) || (tI , E[l := v]) || T →
γ, δ, σ[l 7→ (t, v)], (t0I1 , e) || (tI , E[unit]) || T →
0
γ, δ 0 , σ 0 [l 7→ (t, v)], (t0I1 , e0 ) || (tI , E[unit]) || T .
Assume the lemma holds for sequences of length n, we show that it holds for
sequences of length n + 1. Given a sequence s̄ of length n and a state s such
that end(s̄) → s we write s̄.s to denote the sequence init(s̄) →∗ end(s̄) → s.
Given O(s̄) then we show O(s̄.s). We examine all possible transitions end(s̄)
→ s by cases. We assume the thread (t) being evaluated is a future, as if it is a
continuation or a regular thread then triviallyO(s̄.s) holds.
Case App:
By definition of Corollary 1b.
Case Ref:
By definition of Corollary 3 and C, ∃s̄0 such that P(s̄.s) = s̄0 and O(s̄0 ).
Case Touch:
0
00
We know T = (t0φ , v) || T by rule End Future and ∃si = γ 0 , δ 0 , σ 0 , (t0` , v) || T
such that si ∈ S̄ by definition of O(S̄) and :
γ, δ, σ, (tI , E[touch `]) || T → γ, δ, σ, (tI , E[v]) || T
Therefore O(s̄.s)
Case Spawn:
By definition Corollary 3 and C, ∃s̄0 such that P(s̄.s) = s̄0 and O(s̄0 ).
Case Future:
By definition Corollary 3 and C, ∃s̄0 such that P(s̄.s) = s̄0 and O(s̄0 ).
Case End Future:
By definition Corollary 3 and C, ∃s̄0 such that P(s̄.s) = s̄0 and O(s̄0 ).
Case Read:
We know δ(t)3δ(l) σ(l) = (t0 , v) and
γ, δ, σ, (tI , E[! l]) || (t0I1 , e) || T → γ, δ, σ, (tI , E[v]) || (t0I1 , e) || T
0
By definition of O(s̄) ∃si ∈ s̄ such that γ 0 , δ, σ 0 , (tI , E[! l]) || (t0I1 , E[`]) || T
If e = E[`] the by definition O(s̄.s) otherwise by definition of and 3 we know
δl` ∈
/ {C, FC} and by definition of C we know ∀t0 s.t. δ t0 ` = Ĉ :
δ(t) =
∆t [` 7→ F̂] δ(t0 ) = ∆t0 [` 7→ Ĉ] ∆t < ∆t0 , therefore ∃s̄0 such that P(s̄.s) = s̄0
and O(s̄0 ).
Case Local Read:
By definition Corollary 3 and C, ∃s̄0 such that P(s̄.s) = s̄0 and O(s̄0 ).
Case Write:
We know δ(t)3δ(l) σ(l) = (t0 , v) and
γ, δ, σ, (tI , E[l := v]) || (t0I1 , e) || T → γ, δ, σ, (tI , E[unit]) || (t0I1 , e) || T
0
By definition of O(s̄) ∃si ∈ s̄ such that γ 0 , δ, σ 0 , (tI , E[l := v]) || (t0I1 , E[`]) || T
If e = E[`] the by definition O(s̄.s) otherwise by definition of and 3 we know
δl` ∈
/ {C, FC} and by definition of C we know ∀t0 s.t. δ t0 ` = Ĉ :
δ(t) =
∆t [` 7→ F̂] δ(t0 ) = ∆t0 [` 7→ Ĉ] ∆t < ∆t0 , therefore ∃s̄0 such that P(s̄.s) = s̄0
and O(s̄0 ).
5
Implementation
Implementing safe futures primarily involves the task of enforcing isolation between a future and its continuation to preserve sequential consistency of the
original thread of control. To do this, we utilize a capability scheme that mirrors
our formal semantics. We briefly describe the structure of our prototype implemented in MLton 2 , a whole program optimizing compiler for Standard ML that
has been enriched to support safe futures.
To demonstrate the feasibility of our formal specification, our implementation must be able to handle capabilities efficiently. Instead of generating unique
identifiers for each future that is created, we use ordered thread identifiers. The
implementation allows capabilities to be lifted precisely at the point when an
effect of a future or continuation is witnessed. To determine if an effect can
be witnessed, two capability maps need to be compared. Since capabilities map
thread identifiers to tags, the complexity bound on all operations is proportional
to the number of threads in the system, not the number of shared memory locations. Our implementation optimizes for thread local reads as well as for local
effects. Thus, mutable values that are thread local need not be augmented with
capability maps. Similarly, reading a value a thread has itself written requires
no safety checks.
Disjoint maps are simply merged. Merging two capability maps preserves
thread identifier ordering. If two capability maps are not disjoint, checking their
equality is equivalent to checking the equality of their common mappings. The
two maps can be subsequently merged if their common mappings are equal or
they can be lifted. Instead of keeping separate capability maps for a continuation
and the threads it creates, all threads share one capability map. Thus, when a
future completes, it needs only to check for consistency with this map to ensure
consistency with all of the threads the continuation created. When two capability
maps are merged and ordering constraints must be propagated to a continuation,
only one capability map needs to be updated since all the threads that are a
part of the continuation share a capability map.
When two capability maps cannot be merged, an error is detected and a
continuation must be unrolled and retried. The continuation that needs to be
unrolled is determined by the capability on which the conflict was detected. Since
2
http://www.mlton.org
we utilize thread identifiers, we immediately have a handle to the continuation.
We utilize stabilizers (4) as a lightweight framework for reversion of threads.
Stabilizers allow for the tracking of inter-thread dependencies and can be utilized
to revert all threads that have transitively witnessed an unsafe effect. To allow
top level futures to make progress, we can restrict non-speculative threads to
only lift their capabilities to F. In this way, top-level futures, are prevented from
reading values written by speculative threads (those that have capability C, FC),
and provide a simple means to ensure progress. Top-level futures may only read
values written by other top-level futures and non-speculative threads.
We have tested our prototype implementation on several benchmarks that
compared the cost of safe futures to that of regular threads of control. All experiments were executed on a 16-way 3.2 Ghz AMD Opteron with 32 GB. In our first
experiment we tested the performance of a future and its continuation executing
two depth-first walks of a shared tree undergoing concurrent modifications. The
first walk is executed by the future and the second by the continuation. Our
implementation ensures that updates from the second walk are never visible to
the future, and all of the updates of the future’s walk are visible to the continuation. Each node of the tree is represented by a lightweight server thread which
executes a short communication protocol with every thread that visits the node.
Even when the future (and its continuation) interact with over 30k threads, the
overheads remain less than 17% compared to two regular threads performing
the same traversal, unsafely. Unlike the threaded version of the benchmark, safe
futures ensure that the tree remains consistent. In our second experiment, we
utilized a modified version of STMBench7 (5) written in ML where each node in
the data structure was represented by a lightweight server thread. We measured
the contention created by executing increasingly many futures, all of which perform shared updates to the underlying data-structure compared to executing the
traversals in separate threads of control. Our experiments show that overheads
are around 15% even with 20 concurrent futures (a fairly serious workload) and
increase to about 50% with 30 concurrent futures.
6
Related Work
Concurrent programming in the presence of mutable references can be difficult
and error-prone due to non-deterministic execution. Low-level synchronization
mechanisms such as locks impose a heavy burden on programmers to ensure
safety and liveness. Thus, there have been many recent proposals for higher-level
language abstractions, such as lock-free data structures (6) and transactional
memory (7; 8; 9; 10), to provide useful safety properties like atomicity (11; 12)
and isolation without the use of low-level concurrency control mechanisms. Safe
futures provide a stronger safety property- specifically, deterministic concurrent
execution guaranteed to have the same effect as sequential execution.
Futures are a well known programming construct found in languages from
Multilisp (13) to Java (14). Many recent language proposals (15; 16; 17) incorporate future-like constructs.
Futures typically require that they are manually claimed by explicitly calling
touch. Pratikakis et. al (18) simplify programming with futures by providing
an analysis to track the flow of future through a program and automating the
injection of claim operations on the future at points where the value yielded by
the future is required, although the burden of ensuring safety still rests with the
programmer.
There has been a number of recent proposals dealing with safe futures. Welc
et. al (3) provide a dynamic analysis that enforces deterministic execution of
sequential Java programs. In sequential programs, static analysis coupled with
simple program transformations (1) can ensure deterministic parallelism by providing coordination between futures and their continuations in the presence of
mutable state. Unfortunately neither approach provided safety in the presences
of exceptions. This was remedied in (19; 2) presented an implementation for
exception-handling in the presence of safe futures.
Flanagan and Felleisen (20) presented a formal semantics for futures, but
did not consider how to enforce safety (i.e. determinism) in the presence of mutable state. Navabi and Jagannathan (21) presented a formulation of safe futures
for a higher-order language with first-class exceptions and first-class references.
Neither formulation consider the interaction of futures with explicit threads of
control.
Futures have been extend with support for asynchronous method calls and
active objects (22). Although not described in the context of safe futures, (23)
proposed a type and effect system that simplifies parallel programming by enforcing deterministic semantics. Grace (24) is a highly scalable runtime system that
eliminates concurrency errors for programs with fork-join parallelism by enforcing a sequential commit protocol on threads which run as processes. Boudol and
Petri (25) provide a definition for valid speculative computations independent
of any implementation technique.
7
Conclusion
This paper presents a dynamic analysis for enforcing determinism in an explicitly concurrent program for a higher-order language with references. Safety is
ensured dynamically through the use of a light weight capability tracking mechanism which guarantees that the effects of a continuation are never witnessed
by a future directly or indirectly via interaction with other threads. Our results
suggest that the overhead to ensure such safety is modest, typically only 15%
over an unsafe threaded program.
Acknowledgements: This work is supported by the National Science Foundation under grants CCF-0701832 and CCF-0811631.
References
[1] Navabi, A., Zhang, X., Jagannathan, S.: Quasi-static Scheduling for Safe Futures.
In: PPoPP, ACM (2008) 23–32
[2] Navabi, A., Zhang, X., Jagannathan, S.: Dependence analysis for safe futures. In:
Science of Computer Programming. (2010) To appear.
[3] Welc, A., Jagannathan, S., Hosking, A.: Safe Futures for Java. In: OOPSLA,
ACM (2005) 439–435
[4] Ziarek, L., Schatz, P., Jagannathan, S.: Stabilizers: a modular checkpointing abstraction for concurrent functional programs. In: ICFP, New York, NY, USA,
ACM (2006) 136–147
[5] Guerraoui, R., Kapalka, M., Vitek, J.: Stmbench7: a benchmark for software
transactional memory. In: EuroSys, New York, NY, USA, ACM (2007) 315–324
[6] Rajwar, R., Goodman, J.R.: Transactional Lock-Free Execution of Lock-Based
Programs. In: ASPLOS, ACM (2002) 5–17
[7] Harris, T., Fraser, K.: Language Support for Lightweight Transactions. In: OOPSLA, ACM (2003) 388–402
[8] Herlihy, M., Luchangco, V., Moir, M., III, W.N.S.: Software Transactional Memory
for Dynamic-Sized Data Structures. In: PODC, ACM (2003) 92–101
[9] Shavit, N., Touitou, D.: Software Transactional Memory. In: PODC, ACM (1995)
204–213
[10] Welc, A., Jagannathan, S., Hosking, A.: Transactional Monitors for Concurrent
Objects. In: ECOOP, Springer-Verlag (2004) 519–542
[11] Flanagan, C., Freund, S.N.: Atomizer: a dynamic atomicity checker for multithreaded programs. In: POPL, ACM (2004) 256–267
[12] Flanagan, C., Qadeer, S.: Types for atomicity. In: TLDI, ACM (2003) 1–12
[13] Halstead, R.: Multilisp: A Language for Concurrent Symbolic Computation. ACM
Trans. Program. Lang. Syst. 7 (1985) 501–538
[14] http://java.sun.com/j2se/1.5.0/docs/guide/concurrency.
[15] Allan, E., Chase, D., J. Hallett, V.L., Maessen, J., Ryu, S., Steele, G., TobinHochstadt, S.: The Fortress Language Specification Version 1.0. Technical report,
Sun Microsystems, Inc. (2008)
[16] Charles, P., Grothoff, C., Saraswat, V., Donawa, C., Kielstra, A., Ebcioglu, K., von
Praun, C., Sarkar, V.: X10: an object-oriented approach to non-uniform cluster
computing. In: OOPSLA, ACM (2005) 519–538
[17] Liskov, B., Shrira, L.: Promises: Linguistic Support for Efficient Asynchronous
Procedure Calls in Distributed Systems. In: PLDI, ACM (1988) 260–267
[18] Pratikakis, P., Spacco, J., Hicks, M.: Transparent Proxies for Java Futures. In:
OOPSLA, ACM (2004) 206–223
[19] Zhang, L., Krintz, C., Nagpurkar, P.: Supporting Exception Handling for Futures
in Java. In: PPPJ, ACM (2007) 175–184
[20] Flanagan, C., Felleisen, M.: The semantics of future and an application. Journal
of Functional Programming 9 (1999) 1–31
[21] Navabi, A., Jagannathan, S.: Exceptionally safe futures. In: COORDINATION,
Berlin, Heidelberg, Springer-Verlag (2009) 47–65
[22] de Boer, F.S., Clarke, D., Johnsen, E.B.: A Complete Guide to the Future. In:
ESOP, Springer-Verlag (2007) 316–330
[23] Adve, S.V., Heumann, S., Komuravelli, R., Overbey, J., Simmons, P., Sung, H.,
Vakilian, M.: A type and effect system for deterministic parallel java. In: OOPSLA.
(2009)
[24] Berger, E.D., Yang, T., Liu, T., Novark, G.: Grace: safe multithreaded programming for c/c++. In: OOPSLA, New York, NY, USA, ACM (2009) 81–96
[25] Boudol, G., Petri, G.: A theory of speculative computation. In: ESOP. (2010)
165–184
Download