obstruction-free synchronization
DESCRIPTION
Obstruction-free synchronization. Double-Ended Queues as an example. Article by: Maurice Herlihy , Victor Luchangco , Mark Moir. Presentation : Or Peri. Today’s Agenda. Two obstruction-free, CAS-based implementations of Double-ended queues . Linear array - PowerPoint PPT PresentationTRANSCRIPT
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 arrayo 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.
• 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?
JobJob
Job
JobJobJob
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
JobJobJob
Job
Job
JobJob
• 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/righto 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.valo 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 operationo 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 indexo Left A[k-1]’s last contento Right A[k]’s last content
• This guarantees: o right.val = RNo 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