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
Defenitions (a reminder) A synchronization technique is wait-free if it ensures that every thread will continue to progress in the face of arbitrary delay (or failure) of other threads. It is lock-free if it ensures that some thread always makes progress. It is Obstruction-free if it guarantees progress for any thread that eventually executes in isolation.
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
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
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, ) ) 9) if( CAS(&A[k], cur, ) ) 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, ) ) 10) if( CAS(&A[k-1], cur, ) ) 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, ) ) 9) if( CAS(&A[k], cur, ) ) 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, ) ) 9) if( CAS(&A[k], right, ) ) 10) return k,, ; 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 v 5) if( CAS(&A[k-1], prev, ) ) 6) if( CAS(&A[k], cur, ) ) 7) return “ok”; 8) if( next.val = LN ) //change LN to DN 9) if( CAS(&A[k], cur, ) ) 10) if( CAS(&A[k+1], next, ) ) 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 RN 18) if( CAS(&A[k+2], nextnext, ) ) 11) CAS(&A[k+1], next, ); 12) } //end “if” 13)}//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, ) ) 6) if( CAS(&A[k-1], cur, ) ) 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? ?