Concurrent Queues Gal Milman Based on Chapter 10 (Concurrent Queues and the ABA Problem) in The Art of Multiprocessor Programming by Herlihy and Shavit Seminar 2 (236802) in Advanced Topics in Concurrent Programming Winter 15/16 Instructor: Erez Petrank Queue Head Tail Dequeue Enqueue (Get) (Put) next We will review: • Blocking Queue • Lock-Free Queue Q u e u e o ABA Problem • Synchronous Dual Queue Blocking Queue head Initialization: tail = head = Node(value=null) capacity = input_capacity size = AtomicInteger(0) next tail enqLock, deqLock notFullCondition, notEmptyCondition head next Dummy node value value next next tail Blocking Queue - enq head tail public void addNode(T x) { Node newNode = new Node(x); tail.next = newNode; tail = newNode; } newNode head 1 x 2 tail Blocking Queue - deq head x tail public T removeNodeAndGetValue() { result = head.next.value; head = head.next; return result; } head x tail Blocking Queue - enq public void enq(T x) { boolean mustWakeDequeuers = false; enqLock.lock(); while (size.get() == capacity) notFullCondition.await(); addNode(x); if (size.getAndIncrement() == 0) mustWakeDequeuers = true; enqLock.unlock(); if (mustWakeDequeuers) { deqLock.lock(); notEmptyCondition.signalAll(); deqLock.unlock(); } } Blocking Queue - enq Why should void we lock beforex) signaling? To avoid the lost wakeup problem. public enq(T { boolean mustWakeDequeuers enq = false; enqLock.lock(); deqlock.lock() while (size.get() == capacity) while (queue is empty) notFullCondition.await(); addNode() addNode(x); if (size.getAndIncrement() == 0) notEmptyCondition.signalAll() Why should mustWakeDequeuers = true; notEmptyCondition.await() we lock before enqLock.unlock(); signaling? if (mustWakeDequeuers) { deqLock.lock(); notEmptyCondition.signalAll(); deqLock.unlock(); } } deq is symmetrical. deq We will review: performance • Blocking Queue progress • Lock-Free Queue Q u e u e o ABA Problem • Synchronous Dual Queue Lock-Free Algorithms In the absence of locks: • Use atomic read-modify-write hardware primitives, like CAS • But what about an action that is composed of several atomic primitive calls? o It should mark the data structure, so other threads may finish the action. "lock" "unlock" Lock-Free Queue Initialization: Unbounded ⇒ size and capacity are unnecessary Lock-free ⇒ locks and conditions are unnecessary head tail = head = Node(value=null) next tail * head, tail and Node.next are AtomicReference<Node> Lock-Free Queue - enq public void enq(T x) { Node node = new Node(x); while (true) { Node last = tail.get(); Node next = last.next.get(); if (next == null) { if (last.next.compareAndSet(next, node)) { tail.compareAndSet(last, node); return; } } else { tail.compareAndSet(last, next); } } } Lock-Free Queue - deq public T deq() throws EmptyException { while (true) { Node first = head.get(); Node last = tail.get(); Node next = first.next.get(); if (first != last) { T value = next.value; if (head.compareAndSet(first, next)) return value; } else { if (next == null) throw new EmptyException(); tail.compareAndSet(last, next); } } } ABA Problem • Consider our lock-free queue • But stop relying on Java’s garbage collector • Each thread will maintain a private free list • Now we encounter the ABA problem head a 1 2 deq b head b x free list (2) a tail tail enq(y) head b deq a y tail deq a tail free list (2) head b head a free list (2) b deq tail ABA Solution: A Stamp AtomicStampedReference<Node> encapsulates: • a reference to Node • an integer stamp AtomicReference<Node> replaced by AtomicStampedReference<Node> stamp is incremented in each compareAndSet. We will review: • Blocking Queue • Lock-Free Queue Q u e u e o ABA Problem • Synchronous Dual Queue Synchronous Dual Queue reservation fulfilment Blocking Queue States head vs. Synchronous Dual Queue States head x enq reservations x y tail tail head head tail tail NotEmpty Condition - deq1 - deq2 - … head head deq reservations null tail y fair null tail In the dual queue - enq and deq are symmetrical. Both check queue’s state to figure out if they should be a reserver or a fulfiller. Synchronous Dual Queue • Reserver adds an item to the queue, lock- • Other threads might finish the addition free then spins on the item • Fulfiller changes the item • To end the rendezvous - the item is removed from the queue • rendezvous lock- Other threads might execute the removal free