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