for exam, need to know all different techniques used to pre-empt the problem.
See https://jbesic.ch/slides/session_12.pdf#page=105 for examples

14.1 ABA problem

We will lock at the lock-free stack. However instead of assuming an infinite supply of memory, this time we’ll reuse nodes.
Node pool

14.1.1 Node Pool

We use a stack again, to hold the unused nodes. We then only swap the nodes between the “active” stack and the “inactive” stack (which stores currently non-used nodes).

  • a node just dequeued is put into the node pool
  • to enqueue we take a node from the pool

if the node pool is empty we create a new Node. Otherwise it’s the exact same as the previous stack implementation.

To then use this, we modify the stack code:

14.1.2 ABA Problem

If we use this in production, we’ll see an issue where values don’t match.

  • for testing: push/pop 10k values
  • check the sum pushed value == sum of popped value

This is the ABA Problem in practice.

we lose the B element, because the interrupted thread has a stale reference to the next of the head.

Defined more generally

14.2 Solving the ABA Problem

Easy solutions to the ABA Problem that are “cheating”

  1. DCAS / Multi-Word CAS

    • doesn’t exist on most platforms so we have no choice.
    • DCAS solves the problem by providing the following operation in pop
      • we read head from top
      • then try DCAS(refs=[top, head.next], old=[head, next], new=[next, next])
        • so if top == head and head.next = next swap
      • if fails, re-get head
    • Note: technically we only need double-compare + single swap
  2. Garbage Collection (GC)

    • relies on the existance of a GC
    • even though the GC might give us the same address again not possible to get ABA problem
      • the interrupted thread still holds a reference to the A node, so it can’t get re-used
    • but no GC available in kernel etc.. (to slow as well)

14.2.3 Pointer Tagging

This does not cure the problem, but it delays it significantly.
Just makes it less probable:

  • there are now “32 versions” of each pointer

14.2.4 Hazard Pointers

The issues stems from reusing a pointer that has been read by some thread X but not yet written by CAS.

Idea?

  • before X reads mark it hazardous by entering it into one of the ( number of threads) slots of an array associated with the datastructure
  • when finished remove from the array
    before Y tries to re-use checks all entries of the hazard array

Each thread gets a location in the Hazardous Array can set one pointer to hazardous.

We now have this operation to check if a pointer is hazardous.

only exit the initial do while after having set the head to a hazardous pointer.
re-check if we can put the pointer back into the pool have to call isHazardous again.

How to protect the node pool? The node pool itself suffers from the ABA problem…
Solutions:

  • Thread-local node pools
    • no protection necessary
    • does not help when push/pop are not well balanced
      • they run out of nodes or have to many…
  • Hazard Pointers on the global node pool
    • expensive operation for node reuse
    • equivalent to code above node pool only returns a node only when it is not hazardous
  • Hybrid of both
    • node-local + global node pool
      • we get new nodes from the global pool when imbalance
      • or get rid of them by giving them to the global

(Exam relevant)

Pros

  • actually solves the ABA Problem

Cons

  • doesn’t improve performance in comparison to memory allocation + GC

The hazard-pointers are placed in thread-local storage.
When that is replaced by processor-local storage scales better.

Note: on ARM architectures, the LL/SC operation prevents the ABA problem completely

  • LL/SC pair of instructions(load linked, store conditional)
    • loads a value
    • store-conditional only updates it if no updates have occurred to that location since the load-link.
      gives us lock-free, atomic, read-modify-write operation.

LL/SC is basically hazard pointers in hardware but better.

14.3 Lessons Learned

Lock-free programming: new kinds of problems in comparison to lock-based programming:

  • atomic updates of several pointers / values impossible
    • threads that help each other to guarantee global progress
  • ABA problem (disappears with a garbage collector)
    • beware of ABA on values (not pointers) GC does NOT prevent those