Skip to content

Hazard Pointers — Practice Tasks

All tasks must be solved in Go, Java, and Python. Go: atomic pointers + per-thread hazard slots. Java: AtomicReferenceArray. Python: conceptual model (GIL serializes, but keep the exact step sequence).

Beginner Tasks

Task 1: Implement a single hazard slot with protect/clear. Build a HazardSlot type with protect(loadFn) (publish + validate + retry) and clear(). Demonstrate protecting a pointer read from a shared atomic head.

Go

package main

import "sync/atomic"

type Node struct{ value int; next *Node }

type HazardSlot struct{ /* TODO: atomic.Pointer[Node] */ }

func (h *HazardSlot) Protect(load func() *Node) *Node {
    // TODO: read, publish, validate, retry
    return nil
}
func (h *HazardSlot) Clear() { /* TODO */ }

func main() {
    var head atomic.Pointer[Node]
    head.Store(&Node{value: 7})
    // TODO: protect, print value, clear
}

Java

import java.util.concurrent.atomic.*;

public class Task1 {
    static class Node { int value; Node next; Node(int v){value=v;} }
    // TODO: HazardSlot using AtomicReference<Node>
    public static void main(String[] args) {
        AtomicReference<Node> head = new AtomicReference<>(new Node(7));
        // TODO: protect, print, clear
    }
}

Python

class Node:
    def __init__(self, value, nxt=None):
        self.value, self.next = value, nxt

class HazardSlot:
    def __init__(self): self._ptr = None
    def protect(self, load):
        # TODO: read, publish, validate, retry
        pass
    def clear(self): self._ptr = None

if __name__ == "__main__":
    head = Node(7)
    # TODO: protect, print, clear
  • Constraints: the validate re-read must be present; handle a null load.
  • Expected Output: the protected value, then a cleared slot.
  • Evaluation: correct protect/validate/clear sequence.

Task 2: Implement a retire list and a scan. Maintain a retired []*Node and a scan(slots) that frees (record value of) nodes absent from all slots and keeps the rest. Provide starter code in all 3 languages. - Constraints: scan must keep nodes present in any slot; build a protected set.

Task 3: Detect a use-after-free without hazard pointers. Write a tiny Treiber-stack pop that frees immediately (no HP). Construct a two-thread interleaving (or simulated step trace) where one thread reads a node's next after the other freed it. Print the trace showing the bug. - Constraints: demonstrate the danger window explicitly.

Task 4: Add a hazard pointer to fix Task 3. Take the buggy pop and insert protect → validate → clear so the bug disappears. Show the same interleaving now reads live memory. - Constraints: only the HP additions may change; the bug must be gone.

Task 5: Count protected vs reclaimable nodes. Given a set of hazard slots and a retire list, write a function returning (pinnedCount, freeableCount). Provide starter code in all 3 languages. - Constraints: O(R + H·K) using a set for the protected addresses.

Intermediate Tasks

Task 6: Full Treiber stack with hazard-pointer pop. Implement Push, Pop(slot) (protect/validate/CAS/clear/retire), retire, and scan. Verify pushing 1..5 and popping all yields 5..1 with no use-after-free. Provide starter code in all 3 languages. - Constraints: K = 1; retire instead of free.

Task 7: Threshold-based reclamation. Add a retire threshold of 2·H·K; trigger scan only when the retire list crosses it. Log each scan and how many nodes it freed. - Constraints: show amortization — most retires do not scan.

Task 8: Michael–Scott queue dequeue with 2 slots. Implement a lock-free MS queue dequeue that protects both head and head.next (K = 2), then retires the dummy. Verify FIFO order. - Constraints: both slots used; validate after each protect.

Task 9: O(1) membership scan. Replace a linear scan (O(R·H·K)) with one that builds a hash/identity set of protected pointers (O(R + H·K)). Benchmark both on R = 10⁵, H·K = 64. - Constraints: report the speedup.

Task 10: Slot acquire/release pool. Implement a domain that hands out hazard slots from a reusable pool: acquire() returns a free slot index, release(i) clears and returns it. Show two threads reusing slots. - Constraints: a released slot's pointer must be null before reuse.

Advanced Tasks

Task 11: Concurrent stress test. Run N producer and M consumer goroutines/threads on the hazard-pointer stack for 1e6 operations; assert no value is freed while any slot references it and the multiset of pushed == popped. Provide starter code in all 3 languages. - Constraints: use a race detector (go test -race) where available.

Task 12: Bounded-memory assertion. Instrument the domain to track max retired-but-unfreed nodes during Task 11. Assert it never exceeds H·K + H·R. Plot or log the high-water mark. - Constraints: empirically confirm the Θ(H²·K) bound.

Task 13: Hazard pointers vs reference counting. Implement the same stack with (a) hazard pointers and (b) per-node atomic reference counts. Benchmark pop throughput and peak memory under 8 threads. - Constraints: report read-cost vs memory trade-off.

Task 14: Async background reclaimer. Move scan to a dedicated background thread signaled when pending memory crosses a cap; fall back to inline scan under back-pressure. Show foreground latency drops. - Constraints: foreground threads must never block on scan in the common case.

Task 15: Detect a missing fence. Construct a model (or relaxed-atomics build) where the publish/validate pair has no StoreLoad fence and show the protection can be violated (validate reordered before publish). Then add the fence and show it is fixed. - Constraints: explain why the fence restores safety, referencing the proof.

Benchmark Task

Compare protect/validate hot-path cost across all 3 languages.

Go

package main

import (
    "fmt"
    "sync/atomic"
    "time"
)

type Node struct{ value int; next *Node }

func main() {
    var head atomic.Pointer[Node]
    head.Store(&Node{value: 1})
    var hp atomic.Pointer[Node]
    sizes := []int{1000, 10000, 100000, 1000000}
    for _, n := range sizes {
        start := time.Now()
        for i := 0; i < n; i++ {
            p := head.Load()
            hp.Store(p)
            _ = head.Load() == p // validate
            hp.Store(nil)
        }
        fmt.Printf("iters=%8d: %.3f ns/op\n",
            n, float64(time.Since(start).Nanoseconds())/float64(n))
    }
}

Java

import java.util.concurrent.atomic.AtomicReference;

public class Benchmark {
    public static void main(String[] args) {
        AtomicReference<Object> head = new AtomicReference<>(new Object());
        AtomicReference<Object> hp = new AtomicReference<>();
        int[] sizes = {1_000, 10_000, 100_000, 1_000_000};
        for (int n : sizes) {
            long start = System.nanoTime();
            for (int i = 0; i < n; i++) {
                Object p = head.get();
                hp.set(p);
                boolean ok = head.get() == p; // validate
                hp.set(null);
            }
            long elapsed = System.nanoTime() - start;
            System.out.printf("iters=%8d: %.3f ns/op%n", n, elapsed / (double) n);
        }
    }
}

Python

import timeit

class Box:
    __slots__ = ("p",)

head = Box(); head.p = object()
hp = Box(); hp.p = None
sizes = [1_000, 10_000, 100_000]
for n in sizes:
    def work():
        p = head.p
        hp.p = p
        _ = head.p is p   # validate
        hp.p = None
    t = timeit.timeit(work, number=n)
    print(f"iters={n:>8}: {t/n*1e9:.1f} ns/op")