Obstruction-free synchronization
Article by: Maurice Herlihy, Victor Luchangco,
Mark Moir
Double-Ended Queues as an example
Presentation : Or Peri
Today’s Agenda• Two obstruction-free, CAS-based implementations
of Double-ended queues.
o Linear array
o Circular array
Why Obstruction-Free?
• Avoid locks.
• Non-blocking data sharing between threads.
• Greater flexibility in design compared with Lock-
freedom and wait-freedom implementations.
• In practice, should provide the benefits of wait-
free and lock-free programming.
What’s wrong with Locks?
• Deadlocks
• Low liveness
• Fault-handling
• Scalability
• Obstruction-freedom ensures No thread can be
blocked by delays or failures of other threads.
• Obstruction-free algorithms are simpler, and can
be applied to complex structures.
• It does not guarantee progress when two (or
more) conflicting threads execute concurrently.
• To improve progress one might add a contention
reducing mechanism (“back-off reflex”).
Pros & cons
• lock-free and wait-free implementations use
such mechanisms, but in a way that imposes a
large overhead, even without contention.
• In scenarios with low contention, programming an
Obstruction-free algorithm with some contention-
manager, there’s the benefit from the simple and
efficient design.
Pros & cons
• Double-ended queue- generalize FIFO queues
and LIFO stacks.
DEqueues
• Allows push\pop
operations in both
ends.
• Remember “Job Stealing”?
• One application of DEqueues is as processors’ jobs queues.
• Each processor pops tasks from it’s own Dequeue’s
head.
DEqueues- what for?
Job
Job
Job
Job
Job
Job
Job
• Upon fork(), it pushes tasks to it’s DEqueue‘s head.
• If a processor’s queue is empty, it can “steal”
tasks from another processor’s DEqueue‘s tail.
DEqueues- what for?
Job
Job
Job
Job
Job
Job
Job
Job
• First we’ll see the simpler, linear, array-
based DEqueue.
• Second stage will extend the first one to “wrap
around” itself.
Implementation
• Two special “null” values: LN and RN
• Array A[0,…,MAX+1] holds state.
• MAX is the queue’s maximal capacity.
• INVARIANT: the state will hold: LN+ values* RN+
• An Oracle() function:o Parameter: left/right
o Returns: an array index
• When Oracle(right) is invoked, the returned
index is the leftmost RN value in A.
Implementation – Intro
• Each element i in A has:o A value: i.val
o A version counter: i.ctr
• Version numbers are updated at every CAS
operation.
• Linearization point: point of changing a value in A.
Implementation – Intro
• The Idea: o rightpush(v) will change the leftmost RN to v.
o rightpop() will change the rightmost data to RN (and
return it)
o rightpush(v) returns “full” if there’s a non-RN value at
A[MAX]
o rightpop() returns “empty” if there are neighboring
RN,LN
• Right/left push/pop are symmetric, so we only
show one side.
Implementation – Intro
1) Rightpush(v){2) While(true){3) k := oracle(right);4) prev := A[k-1];5) cur := A[k];6) if(prev.val != RN and cur.val = RN){7) if(k = MAX+1) return “full”;8) if( CAS(&A[k-1], prev,
<prev.val,prev.ctr+1>) )9) if( CAS(&A[k], cur, <v,cur.ctr+1>) )10) return “ok”;11) } //end “if”12) } //end “while”13) } //end func
Implementation – right push
1) Rightpop(){2) While(true){3) k := oracle(right);4) cur := A[k-1];5) next := A[k];6) if(cur.val != RN and next.val = RN){7) if(cur.val = LN and A[k-1] = cur) 8) return “empty”;9) if( CAS(&A[k], next, <RN, next.ctr+1>) )10) if( CAS(&A[k-1], cur, <RN,cur.ctr+1>)
)11) return cur.val;12) } //end “if”13) } //end “while”14) } //end func
Implementation – right pop
• Relies on three claims: o In a rightpush(v) operation, at the moment we “CAS“
A[k].val from an RN value to v, A[k-1].val is not RN.
o In a rightpop() operation, at the moment we “CAS” A[k-
1].val from some v to RN, A[k].val contains RN.
o If rightpop() returns “empty”, then at the moment it
performed next:=A[k] (and just after: cur:=A[k-1]),
these two values were LN and RN.
Linearizability
• The third claim:o If rightpop() returns “empty”, then at the moment it
performed next:=A[k] (and just after: cur:=A[k-1]),
these two values were LN and RN.
• holds since: 4) cur := A[k-1];5) next := A[k];6) if(cur.val != RN and next.val = RN){7) if(cur.val = LN and A[k-1] = cur) 8) return “empty”;
• A[k-1] didn’t change version number from line 4 to 7
• so did A[k] from line 5 to 6.
Linearizability
• The first two claims hold similarly:o Since CAS operations check version numbers, only if no one
interfered with another push/pop, we can perform the operation
o In rightpush(v) for example:
4) prev := A[k-1];5) cur := A[k];6) if(prev.val != RN and cur.val = RN){7) if(k = MAX+1) return “full”;8) if( CAS(&A[k-1], prev, <prev.val,prev.ctr+1>) )9) if( CAS(&A[k], cur, <v,cur.ctr+1>) )
• Counter didn’t change (upon success) from line 5 to 9,
hence so did the value.
• Same holds for the neighbor (k-1) from line 4 to 8
Linearizability
• Implementing the Oracle() function:o For linearizability, we only need oracle() to return an index at range.
o For Obstruction-freedom we have to show that it is eventually
accurate if invoked repeatedly without interference.
• Naïve approach is to simply go over the entire
array and look for the first RN.
• Another approach is to keep “hints” (last
position, for instance), and search around them.
• We can update these hints frequently or seldom
with respect to cache locations… but that’s off-topic
Linearizability
• The Idea: o A[0] is “immediately to the right” of A[MAX+1].
o All indices are calculated modulo MAX+2.
• Two main differences:o To return “full” we must be sure there are exactly two null entries.
o A rightpush operation may encounter a LN value we’ll convert them
into RN values (using another null character: DN).
Extension to circular array
• All null values are in a contiguous sequence in the
array.
• This sequence is of the form: RN* DN* LN*
• There are at least 2 different types of null values
in the sequence.
Circular array - Invariants
• We don’t invoke oracle(right) directly.
• Instead, we have rightCheckOracle() which
returns:
o K an array index
o Left A[k-1]’s last content
o Right A[k]’s last content
• This guarantees:
o right.val = RN
o Left.val != RN
Circular array - Implementation
rightCheckedOracle()1) While(true){2) k := oracle(right);3) left := A[k-1];4) right := A[k];5) if(right.val = RN and left.val != RN)6) return k,left,right;7) if( right.val = DN and !(left.val in {RN,DN})
)8) if( CAS(&A[k-1], left, <left.val,
left.ctr+1>) )9) if( CAS(&A[k], right,
<RN,cur.ctr+1>) )10) return k,<left.val,left.ctr+1>, <RN,right.ctr+1>;11) } //end “while”
• The array is not “full” when A[k+1] is RN.
• this is since A[k] is RN and an Invariant holds
that “There are at least 2 different types of null
values in the sequence”.
• So, if A[k+1] = LN try converting it to DN
• If A[k+1] = DN try converting it to RN
• In this case, we need to check “nextnext”.
The major change – rightPush(v)
rightPush(v)1) While(true){2) k,prev,cur := rightCheckedOracle();3) next := A[k+1];4) if( next.val = RN ) //change RN to v5) if( CAS(&A[k-1], prev,
<prev.val,prev.ctr+1> ) )6) if( CAS(&A[k], cur, <v,cur.ctr+1>) )7) return “ok”;8) if( next.val = LN ) //change LN to DN9) if( CAS(&A[k], cur, <RN, cur.ctr+1>) )10) if( CAS(&A[k+1], next,
<DN,next.ctr+1>) )11) if(next.val = DN)
rightPush(v)11) if(next.val = DN){12) nextnext:= A[k+2];13) if( !(nextnext.val in {RN,LN,DN}) )14) if(A[k-1] = prev)15) if(A[k] = cur)16) return “full”;17) if( nextnext.val = LN) //DN to RN18) if( CAS(&A[k+2], nextnext, <nextnext.val,nextnext.ctr+1>) )19) CAS(&A[k+1], next,
<RN,next.ctr+1>);20) } //end “if”21)}//end “while”
rightPop()1) While(true){2) k,cur,next := rightCheckedOracle();3) if( cur.val in {LN,DN} and A[k-1] = cur )4) return “empty”;5) if( CAS(&A[k], next, <RN, next.ctr+1>) )6) if( CAS(&A[k-1], cur,
<RN,cur.ctr+1>) )7) return cur.val;8) }//end “while”
• Is harder to prove in this case (there’s a whole
other article just to do so).
• The main difficulty: proving that when
rightPush(v) changes a value, it has an RN or an
DN to it’s right.
• There are 5 lines in the code (of the right side
functions) which may interrupt with this, but they
are all using CAS, and intuitively, the .ctr values
should assure correctness.
Linearizability
• We’ve seen Two Obstruction-free implementations
of a Dequeue.
• As promised, they are pretty simple.
• Hopefully, I’ve managed to demonstrate the main
degradation, as well as an intuition as to why it’s
a good solution for relatively low contention
scenarios
To Sum up
Questions?
?
Top Related