Case study: An example on non-linear effects of scheduling: "Receive livelocks in interrupt-driven kernel"

Multi-processor coordination without locks

How to avoid using locks. Even with scalable locks, their use can be expensive. Let's start with a simple data structure and try to allow for concurrent access, so that we get a feel for the issues.

Searchable stack using locks

	struct element {
	    int key;
	    int value;
	    struct element *next;
	};

	struct element *top;

	void push(struct element *e) {
	    e->next = top;
	    top = e;
	}

	struct element *pop(void) {
	    struct element *e = top;
	    top = e->next;
	    return e;
	}

	int search(int key) {
	    struct element *e = top;
	    while (e) {
		if (e->key == key)
		    return e->value;
	        e = e->next;
	    }
	    return -1;
	}

This is clearly not going to work on a concurrent system

global spinlock: performance how many concurrent ops? just one CPU at a time bus interactions? bounce cache line for both reads, writes

global read-write lock: performance

is it going to get better if we allocate a read-write lock for each item?

more scalable locking schemes

Avoiding locks

why do we want to avoid locks?

Recall compare-and-swap:

	int cmpxchg(int *addr, int old, int new) {
	    int was = *addr;
	    if (was == old)
		*addr = new;
	    return was;
	}

Usage:

	void push(struct element *e) {
    do {
	    e->next = top;
		} while (cmpxchg(&top, e->next, e) != e->next);
	}

	struct element *pop(void) {
    do {
	    struct element *e = top;
		} while (cmpxchg(&top, e, e->next) != e);
	  return e;
	}
What about search? Can be the same as in the non-concurrent case?

But before that: why is this better than not having locks?

problem 1: lock-free data structures require careful design, hw support

problem 2: memory reuse
when can we free a memory block, if other CPUs could be accessing it? other CPU's search might be traversing any of the elements

What if the memory is freed (and reused) while a search() thread is still holding a reference to it?

Worse, reusing a memory block can corrupt list

Memory reuse should be prevented till one is sure that no thread could be holding a reference to it (in its stack or registers), i.e., free() should be prevented till the time any thread could be holding a reference to something that was just popped off.

But how do we know about when it is safe to reuse memory? Would waiting for a long time (say one hour) before freeing work? Well almost -- at least the probability that another thread could be holding a reference to such a location becomes smaller with time, and so the probability of a failure is very low. How does one decide how long to wait before reusing memory? If too small, can result in incorrect behaviour. If too large, cannot reuse memory for a long time, which can be a problem is the available memory is small.