View
233
Download
1
Category
Tags:
Preview:
Citation preview
Outline:Some definitions
3 queue implementations :
A Bounded Partial Queue
An Unbounded Total Queue
An Unbounded Lock-Free Queue
Introduction and some definitions :
Pools show up in many places in concurrent systems. For example, in many applications, one or more producer threads produce items to be consumed by one or more consumer threads.
To allow consumers to keep up, we can place a buffer between the producers and the consumers.
Often, pools act as producer–consumer buffers.A pool allows the same item to appear more
than once.
Introduction and some definitions cont .
A queue is a special kind of pool with FIFO fairness.
It provides an enq(x) method that puts item x at one end of the queue, called the tail, and a deq() method that removes and returns the item at the other end of the queue, called the head.
Bounded vs. Unbounded
A pool can be bounded or unbounded.Bounded
Fixed capacityGood when resources an issue
UnboundedHolds any number of objects
Blocking vs. Non-BlockingProblem cases:
Removing from empty poolAdding to full (bounded) pool
BlockingCaller waits until state changes
Non-BlockingMethod throws exception
Total vs. Partial Pool methods may be total or partial.A method is total if calls do not wait for
certain conditions to become true. For example, a get() call that tries to remove an item from an empty pool immediately returns a failure code or throws an exception. A total interface makes sense when the producer (or consumer) thread has something better to do than wait for the method call to take effect.
Total vs. Partial A method is partial if calls may wait for
conditions to hold. For example, a partial get() call that tries to remove an item from an empty pool blocks until an item is available to return. A partial interface makes sense when the producer (or consumer) has nothing better to do than to wait for the pool to become nonempty (or non full).
A Bounded Partial Queue
head
tail
deqLock
enqLock
Permission to enqueue 8 items
permits
8
Lock out other enq() calls
Lock out other deq() calls
First actual itemSentinel
Enqueuer
head
tail
deqLock
enqLock
permits
8 Release lock7
If queue was empty, notify waiting
dequeuers
Dequeuer
head
tail
deqLock
enqLock
permits
8
Increment permits(no need
lock?)Answer: we had to hold the lock while enqueuing to prevent lots of enqueuers from proceeding without noticing that the capacity had been exceeded. Dequeuers will notice the queue is empty when they observe that the sentinel’s next field is null
Bounded Queue
public class BoundedQueue<T{ > ReentrantLock enqLock, deqLock;
Condition notEmptyCondition, notFullCondition; AtomicInteger permits;
Node head ; Node tail ;
int capacity; enqLock = new ReentrantLock;)(
notFullCondition = enqLock.newCondition;)( deqLock = new ReentrantLock;)(
notEmptyCondition = deqLock.newCondition;)(}
The ReentrantLock is a monitor (The mechanism that Java uses to
support synchronization ). Allows blocking on a condition rather than spinning.
How do we use it?
(*More on monitors: http://www.artima.com/insidejvm/ed2/threadsync
h.html)
Lock Conditions
public interface Condition{ void await;)(
boolean await(long time, TimeUnit unit);…
void signal ;)( void signalAll;)(
}
Await
Releases lock associated with qSleeps (gives up processor)Awakens (resumes running)Reacquires lock & returns
q.await()
Yawn!
Monitor Signalling
Cri
tical S
ecti
on
waiting room
Yawn!
Awakend thread might still lose lock to
outside contender …
Enq Methodpublic void enq(T x){
boolean mustWakeDequeuers = false ; enqLock.lock;)(
try { while (permits.get() == 0)
notFullCondition.await ;)( Node e = new Node(x);
tail.next = e; tail = e;
if (permits.getAndDecrement() == capacity) mustWakeDequeuers = true;
} finally{ enqLock.unlock;)(
} … }
Cont…
public void enq(T x){ …
if (mustWakeDequeuers){ deqLock.lock;)(
try{ notEmptyCondition.signalAll;)(
} finally{ deqLock.unlock;)(
} } }
The Enq() & Deq() MethodsShare no locks
That’s goodBut do share an atomic counter
Accessed on every method callThat’s not so good
Can we alleviate this bottleneck?
What is the problem?
Split the CounterThe enq)( method
Decrements onlyCares only if value is zero
The deq)( methodIncrements onlyCares only if value is capacity
Split CounterEnqueuer decrements enqSidePermitsDequeuer increments deqSidePermitsWhen enqueuer runs out
Locks deqLockTransfers permits(dequeuer doesn't need permits- check
head.next)Intermittent(תקופתי) synchronization
Not with each method callNeed both locks! (careful …)
An Unbounded Total Queue
Queue can hold an unbounded number of items.
The enq() method always enqueues its item.
The deq() throws EmptyException if there is no item to dequeue.
No deadlock- each method acquires only one lock.
Both the enq() and deq() methods are total as they do not wait for the queue to become empty or full.
A Lock-Free Queue
Sentinelhead
tail
•Extension of the unbounded total queue•Quicker threads help the slower threads•Each node’s next field is an: AtomicReference<Node>•The queue itself consists of two AtomicReference<Node> fields: head and tail
EnqueueThese two steps are not atomicThe tail field refers to either
Actual last Node (good)Penultimate* Node (not so good)
Be prepared!
(*Penultimate :next to the last)
EnqueueWhat do you do if you find
A trailing tail?Stop and fix it
If tail node has non-null next fieldCAS the queue’s tail field to tail.next
Enq()Creates a new node with the new value to be enqueuedreads tail, and finds the node that appears to be lastchecks whether that node has a successorIf not - appends the new node by calling compareAndSet()If the compareAndSet() succeeds, the thread uses a
second compareAndSet() to advance tail to the new nodesecond compareAndSet() call fails, the thread can still
return successfullyIf the tail node has a successor , then the method tries to
“help”other threads by advancing tail to refer directly to the successor before trying again to insert its own node.
Deq()If the queue is nonempty(the next field of the
head node is not null), the dequeuer calls compareAndSet() to change head from the sentinel node to its successor
before advancing head one must make sure that tail is not left referring to the sentinel node which is about to be removed from the queue
test: if head equals tail and the (sentinel) node they refer to has a non-null next field, then the tail is deemed to be lagging behind.
deq() then attempts to help make tail consistent by swinging it to the sentinel node’s successor , and only then updates head to remove the sentinel
SummaryA thread fails to enqueue or dequeue a
node only if another thread’s method call succeeds in changing the reference, so some method call always completes.
As it turns out, being lock-free substantially enhances the performance of queue implementations, and the lock-free algorithms tend to outperform the most efficient blocking ones.
Recommended