Lecture 06: Concurrent Objects 1

Outline

  1. Finishing Up Mutex:
    • Bakery Algorithm
    • Lower Bounds
  2. Concurrent Objects

Last Time

Lamport’s Bakery Algorithm

public void lock () {
    int i = ThreadID.get();
    flag[i] = true;
    label[i] = max(label[0], ..., label[n-1]) + 1;
    while (!hasPriority(i)) {} // wait	
}

public void unlock() {
    flag[ThreadID.get()] = false;
}

We Showed:

  1. Deadlock-freedom
    • unique thread with highest priority obtains lock
  2. First-come-first-served (FCFS) Property
    • if i writes label[i] before j writes label[j], then i obtains lock before j

Starvation-freedom follows from 1 and 2

To Show: Mutual Exclusion

public void lock () {
    int i = ThreadID.get();
    flag[i] = true;
    label[i] = max(label[0], ..., label[n-1]) + 1;
    while (!hasPriority(i)) {} // wait	
}

Suppose not:

  • $A$ and $B$ concurrently in CS
  • Assume: $(\mathrm{label}(A), A) < (\mathrm{label}(B), B)$

Proof (Continued)

Since $B$ entered CS:

  • Must have read
    • $(\mathrm{label}(B), B) < (\mathrm{label}(A), A)$, or
    • $\mathrm{flag}[A] == \mathrm{false}$
  • Former can not happen: labels strictly increasing

  • So $B$ read $\mathrm{flag}[A] == \mathrm{false}$

Compare Timelines!

Conclusion

Lamport’s Bakery Algorithm:

  1. Works for any number of threads
  2. Satisfies MutEx and starvation-freedom

Question

Is the bakery algorithm practical?

  • Maybe for few threads…
  • But for many threads?
    • label array contains $n$ indices
    • must read all entries to set own label
    • costly if many threads!
  • Could we do better?

Remarkably

We cannot do better:

  • If $n$ threads want to achieve mutual exclusion + deadlock-freedom, must have $n$ read/write registers (variables)

  • This is really bad if we have a lot of threads!
    • 1,000 threads means each call to lock() requires 1,000s of reads
    • each call to hasPriority requires either 1,000 of reads or a more advanced data structure
  • Things are messy!

A Way Around the Lower Bound

  • Argument relies crucially on fact that the only atomic operations are read and write
  • Modern computers offer more powerful atomic operations
  • In Java, AtomicBoolean class offers, e.g.,
    • compareAndSet(boolean expectedValue, boolean newValue)
    • getAndSet(boolean newValue)
  • These operations are useful, but still costly
  • We will discuss more later

Concurrent Objects

So Far

Considered mutual exclusion:

  • Restrict access to critical section of code to one thread at a time
  • This makes sense for simple objects
    • e.g., the increment() method in Counter

What about larger data structures?

Linked Lists

Recall: a (doubly) linked list

Insertion 1

Insertion 2

Linked List in Code

public class MyLinkedList {
    private Node head;
	...
	
    // insert a new node after nd
    public void insert (Node nd, value) {
        Node next = nd.getNext();
        Node cur = new Node(value);
        nd.next = cur;
        cur.prev = nd;
        cur.next = next;
        if (next != null) next.prev = cur;
    }
}

class Node {
    public Node next;
    public Node prev;
    public int value;
}

Insertion with Multiple Threads

What could go wrong?

    public void insert (Node nd, value) {
        Node next = nd.getNext();
        Node cur = new Node(value);
        nd.next = cur;
        cur.prev = nd;
        cur.next = next;
        if (next != null) next.prev = cur;
    }

How to Fix The Problem?

    public void insert (Node nd, value) {
        Node next = nd.getNext();
        Node cur = new Node(value);
        nd.next = cur;
        cur.prev = nd;
        cur.next = next;
        if (next != null) next.prev = cur;
    }

A Fix: Locking the List

public class MyLinkedList {
    private Node head;
    private Lock lock;
	...
	
    // insert a new node after nd
    public void insert (Node nd, value) {
        lock.lock();
        try { // all of this is critical section
            Node next = nd.getNext();
            Node cur = new Node(value);
            nd.next = cur;
            cur.prev = nd;
            cur.next = next;
            if (next != null) next.prev = cur;
        } finally {
            lock.unlock();
        }
    }
}

Illustration of Locked Execution

Red Acquires Lock

Red Inserts Element

Red Releases Lock

Blue Acquires Lock

Blue Inserts Element

Blue Releases Lock

Nice…

…but…

…Could we Have Done Better?

How?

When Can We Insert Concurrently?

What Should we Lock?

Not the whole list!

Idea: Locking Individual Nodes

Which nodes need to be locked?

Locking Nodes in Code

class Node {
    private Lock lock;
    public Node next;
    public Node prev;
    public int value;
	
    public void lock() { lock.lock(); }
    public void unlock() { lock.unlock(); }
}

Insertion with Locked Nodes

    public void insert (Node nd, value) {
        Node cur = new Node(value);
		
        nd.lock();
        try {
            Node next = nd.getNext();
            if (next != null) next.lock();
            nd.next = cur;
            cur.prev = nd;
            cur.next = next;
            if (next != null) next.prev = cur;
        } finally {
            if (next != null) next.unlock();
            nd.unlock();
        }
    }

Concurrent Insertions

Acquiring Locks

Both Insert

Both Release

What Happens with Contention?

Red Acquires Locks (Blue Waits)

Red Inserts & Releases Locks

Blue Finally Acquires Locks

Blue Inserts & Releases Locks

This Seems Pretty Good!