Obstruction-free synchronization Article by: Maurice Herlihy, Victor Luchangco, Mark Moir...

Post on 20-Dec-2015

214 views 0 download

Transcript of Obstruction-free synchronization Article by: Maurice Herlihy, Victor Luchangco, Mark Moir...

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?

?