Hubbry Logo
Hazard pointerHazard pointerMain
Open search
Hazard pointer
Community hub
Hazard pointer
logo
7 pages, 0 posts
0 subscribers
Be the first to start a discussion here.
Be the first to start a discussion here.
Hazard pointer
Hazard pointer
from Wikipedia

In a multithreaded computing environment, hazard pointers are one approach to solving the problems posed by dynamic memory management of the nodes in a lock-free data structure. These problems generally arise only in environments that don't have automatic garbage collection.[1]

Any lock-free data structure that uses the compare-and-swap primitive must deal with the ABA problem. For example, in a lock-free stack represented as an intrusively linked list, one thread may be attempting to pop an item from the front of the stack (A → B → C). It remembers the second-from-top value "B", and then performs compare_and_swap(target=&head, newvalue=B, expected=A). Unfortunately, in the middle of this operation, another thread may have done two pops and then pushed A back on top, resulting in the stack (A → C). The compare-and-swap succeeds in swapping `head` with `B`, and the result is that the stack now contains garbage (a pointer to the freed element "B").

Furthermore, any lock-free algorithm containing code of the form

Node* currentNode = this->head;  // assume the load from "this->head" is atomic
Node* nextNode = currentNode->next;  // assume this load is also atomic

suffers from another major problem, in the absence of automatic garbage collection. In between those two lines, it is possible that another thread may pop the node pointed to by this->head and deallocate it, meaning that the memory access through currentNode on the second line reads deallocated memory (which may in fact already be in use by some other thread for a completely different purpose).

Hazard pointers can be used to address both of these problems. In a hazard-pointer system, each thread keeps a list of hazard pointers indicating which nodes the thread is currently accessing. (In many systems this "list" may be probably limited to only one[1][2] or two elements.) Nodes on the hazard pointer list must not be modified or deallocated by any other thread.

Each reader thread owns a single-writer/multi-reader shared pointer called "hazard pointer." When a reader thread assigns the address of a map to its hazard pointer, it is basically announcing to other threads (writers), "I am reading this map. You can replace it if you want, but don't change its contents and certainly keep your deleteing hands off it."

— Andrei Alexandrescu and Maged Michael, Lock-Free Data Structures with Hazard Pointers[2]

When a thread wishes to remove a node, it places it on a list of nodes "to be freed later", but does not actually deallocate the node's memory until no other thread's hazard list contains the pointer. This manual garbage collection can be done by a dedicated garbage-collection thread (if the list "to be freed later" is shared by all the threads); alternatively, cleaning up the "to be freed" list can be done by each worker thread as part of an operation such as "pop" (in which case each worker thread can be responsible for its own "to be freed" list).

In 2002, Maged Michael of IBM filed an application for a U.S. patent on the hazard pointer technique,[3] but the application was abandoned in 2010.

Alternatives to hazard pointers include reference counting.[1]

Hazard pointers were added to C++26 as std::hazard_pointer which is a single-writer multi-reader pointer that can be owned by at most one thread at any point of time, in header <hazard_pointer>, for C++ Standard Library threading support. To be hazard-protectable, a class must extend a class called std::hazard_ptr_obj_base.[4]

See also

[edit]

References

[edit]
[edit]
Revisions and contributorsEdit on WikipediaRead on Wikipedia
from Grokipedia
Hazard pointers are a synchronization primitive in concurrent programming designed to enable safe memory reclamation for dynamic lock-free data structures, allowing threads to protect shared objects from premature deallocation while avoiding the need for locks or garbage collection. Introduced by Maged M. Michael in 2004, this technique addresses the challenges of memory management in multithreaded environments where threads concurrently read and modify linked data structures, such as queues and stacks, without blocking each other. The core mechanism involves each thread maintaining a fixed number of hazard pointers—special shared variables that point to objects the thread is currently accessing or may soon access. When a thread retires (potentially deletes) an object, it adds it to a local list and periodically scans all active hazard pointers across threads; if the retired object is not referenced by any hazard pointer, it can be safely reclaimed, ensuring constant expected amortized for scans. This approach mitigates issues like the , where a reused could lead to incorrect operations, and supports scalable, non-blocking concurrency without hardware-specific instructions. Hazard pointers have been widely adopted in high-performance systems, including implementations in libraries like Facebook's for concurrent hash maps and sets, where they facilitate fast, lock-free iterations and read-mostly access patterns. Hazard pointers are included in C++26 via paper P2530R3 (as of 2025), providing a stable interface in the <hazard_pointer> header to simplify their use in portable code, with classes like std::hazard_pointer_obj_base for object protection and functions for domain management. Their integration into the standard aims to promote broader adoption in lock-free algorithms, enhancing concurrency in applications from databases to real-time systems.

Background and motivation

Problems in lock-free concurrent programming

Lock-based approaches to concurrent programming rely on primitives, such as mutexes, to serialize access to shared s, ensuring but introducing risks like deadlocks, , and convoying effects where threads block each other unnecessarily. In contrast, lock-free methods eschew locks entirely, using atomic operations like (CAS) to enable concurrent modifications while guaranteeing non-blocking progress: if multiple threads are active on the , at least one will complete its operation in a finite number of steps, preventing system-wide stalls even if individual threads experience repeated failures. This progress property stems from the inherent resilience of atomic primitives to delays or failures, making lock-free algorithms more robust in asynchronous environments compared to lock-based ones, which can halt entirely due to blocking. A prominent challenge in lock-free programming is the , which arises during CAS operations when a shared value appears unchanged between reads but has been modified intermediately by other threads, leading to spurious successes and . Consider a lock-free removal: Thread T1 reads a node's next pointer as null (value A) to confirm it is the tail and prepares a CAS to update the previous node's next to null; concurrently, Thread T2 removes the node (changing the value to some other pointer B), frees it, reuses the memory for a new node with next set back to null (value A again), and inserts it elsewhere. When T1 resumes and performs the CAS, it succeeds incorrectly because the value matches A, but the structure now points to the wrong node, potentially orphaning elements or creating cycles. This issue is exacerbated in environments without garbage collection, where memory reuse is common, and it undermines the of operations unless mitigated by techniques like version counters appended to pointers. Beyond ABA, lock-free data structures face general race conditions in pointer manipulations, where concurrent atomic updates to shared pointers can result in lost nodes, duplicate insertions, or invalid traversals without additional beyond CAS. For instance, in a concurrent queue, multiple threads attempting to swing a tail pointer via CAS may overwrite each other's updates if not carefully ordered, leading to broken links or enqueued items becoming inaccessible. Early attempts at such structures, like those by Stone (1990) and Valois (1995), encountered races in pointer swings that caused list fragmentation or item loss, highlighting the subtlety required in designing atomic pointer operations. The development of lock-free structures began in the late and , with foundational work including Treiber's 1986 lock-free stack algorithm, which used CAS on a top pointer for push and pop operations in a singly-linked list, establishing a simple non-blocking pattern for LIFO access. This was followed by efforts on queues, such as incomplete algorithms by Hwang and Briggs (1987) and Sites (1989), which suffered from specification gaps and races. The seminal Michael and Scott queue (1996) addressed these by combining CAS with careful pointer tagging and validation steps, achieving practical lock-free FIFO behavior, though it still grappled with issues like the and . These early structures demonstrated the viability of lock-free programming but underscored persistent challenges in ensuring correctness without locks, particularly around dynamic memory and concurrent pointer updates. One specific unsolved issue in these designs was safe memory reclamation for retired nodes, as threads might retain dangling references amid ongoing operations.

The memory reclamation challenge

In lock-free concurrent data structures, such as linked lists and search trees, nodes are dynamically allocated and deallocated by multiple threads operating simultaneously without mutual exclusion. This concurrent memory management is essential for scalability but introduces the risk of premature reclamation, where a thread removes and frees a node while another thread still holds a pointer to it. If the freed memory is reused for a different purpose, the second thread may dereference a dangling pointer, leading to data corruption, crashes, or undefined behavior. Garbage collection provides automatic by tracking reachable objects but is unsuitable for real-time or high-performance lock-free systems due to its non-deterministic pause times, which can halt all threads and violate timing guarantees. These pauses arise from stop-the-world phases needed to scan and compact , making garbage collection incompatible with applications requiring bounded latency, such as embedded systems or high-throughput servers. Reference counting, an alternative where each node maintains a counter of active pointers, fails in lock-free settings due to atomicity challenges and potential for incomplete reclamation. Updating the count atomically during concurrent reads and writes requires strong multi-word primitives that are unavailable on most hardware, leading to inefficiency and . Moreover, circular references among nodes can prevent the count from reaching zero, even for unreachable objects, blocking reclamation indefinitely. The can exacerbate these issues by allowing a node to appear safe for reuse when its pointer value matches a prior state, though memory reclamation risks compound the broader challenges.

Core concept

Hazard pointers definition

Hazard pointers are a form of per-thread, single-writer multi-reader atomic pointers designed to facilitate safe reclamation in lock-free concurrent structures. Each thread maintains a small, fixed number of these pointers—typically one or two—to reference nodes that it may access during operations on dynamic objects, such as linked lists or queues. These pointers are implemented using standard atomic operations, such as single-word atomic reads and writes, ensuring that only the owning thread can modify its hazard pointers while all threads can read any of them. Key properties of hazard pointers include their global visibility across all threads and initial null values, which signal that no protection is active. In the context of optimistic concurrency, a thread "publishes" a hazard pointer by atomically setting it to the address of a node before performing a read or traversal, thereby announcing to other threads that the node is in active use and should not be reclaimed. This mechanism allows threads to proceed without locks while guaranteeing that protected nodes remain valid during the operation. For example, during an enqueue operation in a lock-free FIFO queue, a thread might first acquire a local reference t to a new tail node, then set its first hazard pointer hp0 atomically with *hp0 = t before validating and linking the node. This simple assignment ensures the node is protected from immediate reuse by any reclaiming thread.

Publisher-subscriber paradigm

In the hazard pointer mechanism, the publisher-subscriber paradigm facilitates safe memory reclamation by enabling threads to announce their intent to access shared objects while allowing other threads to verify the safety of deallocation operations. Threads act as publishers by setting their hazard pointers to reference specific nodes they plan to read or traverse, thereby notifying all other threads of potential ongoing access to those nodes. This publication step ensures visibility across the system without requiring centralized coordination. Conversely, threads attempting to retire or reclaim memory nodes serve as subscribers by scanning and checking the set of all active hazard pointers maintained by publishers. If a retired node does not match any published hazard pointer, the subscriber can proceed with deallocation, as no other thread is currently accessing it. This subscription process decouples the protection announcements from the reclamation decisions, promoting lock-free progress. The paradigm relies on single-writer multi-reader semantics for each hazard pointer, where only the owning thread (publisher) can update its value, while any thread (subscriber) can read it concurrently. This ownership model limits contention to the writer's updates and supports efficient reads by multiple subscribers. Atomicity in hazard pointer updates is critical to maintain consistency, as publishers use atomic write operations to set pointers without intermediate states that could lead to races. Subscribers similarly rely on atomic reads to observe the latest published values, ensuring that the coordination remains reliable in a non-blocking environment.

Operation

Protecting objects

In hazard pointers, protecting an object involves a thread announcing its intent to access a specific node by atomically a pointer to it in one of its designated hazard pointer slots, ensuring that the node cannot be reclaimed by other threads during the access period. This process relies on the publisher-subscriber , where the accessing thread acts as the publisher by updating its local hazard pointer. The protection sequence begins when a thread reads a pointer to a node from a concurrent , such as during a traversal in a lock-free . To safeguard this node, the thread atomically sets one of its hazard pointers to point to the read node. Once set, the thread can safely perform read or write operations on the node, as any potential will detect the hazard pointer and defer reclamation. After completing the access, the thread clears the hazard pointer by atomically setting it to null, releasing the protection. For data structures requiring traversal of multiple linked nodes, such as doubly-linked lists or trees, each thread maintains multiple hazard pointer slots—commonly one or two—to protect both the current node and its successor or predecessor simultaneously. This allows safe navigation without unprotected intermediate reads that could lead to dangling pointers. The number of slots per thread is fixed and chosen based on the maximum concurrency and complexity, balancing protection needs with space efficiency. The following pseudocode illustrates the basic protection mechanism for a single node in a lock-free context:

function protect_and_access(node_ptr p): thread_hazard[slot] = p // Atomic set // Now p is protected; safely access the node value = p->data // Example read operation // ... perform other operations on p ... thread_hazard[slot] = null // Atomic clear return value

function protect_and_access(node_ptr p): thread_hazard[slot] = p // Atomic set // Now p is protected; safely access the node value = p->data // Example read operation // ... perform other operations on p ... thread_hazard[slot] = null // Atomic clear return value

This approach ensures race-free , as the atomic writes to the thread's own slots are contention-free, while the global list of hazard pointers (maintained separately) allows deferring reclamation without locks.

Retiring and reclaiming nodes

In the hazard pointer methodology, when a thread logically removes a node from a concurrent data structure, it does not immediately deallocate the memory to avoid use-after-free errors from concurrent accesses. Instead, the thread retires the node by appending it to a thread-local list of retired nodes, often denoted as rlist, which accumulates potentially reusable objects. To bound the memory overhead and trigger periodic reclamation, each thread monitors the size of its rlist against a threshold R=HlogHR = H \log H, where HH is the maximum number of hazard pointers that can be active across all threads (typically H=N×KH = N \times K, with NN threads and KK pointers per thread). When the local retired count reaches or exceeds RR, the thread invokes a scan operation to identify safe-to-reclaim nodes. This threshold ensures that reclamation efforts occur infrequently enough to amortize costs while preventing unbounded list growth. The scanning algorithm proceeds in two stages to determine which retired nodes can be safely reclaimed. In the first stage, the thread collects all currently active hazard pointers by iterating over the shared of hazard pointer records, inserting non-null values into a local list or (denoted plist) for efficient lookup. This captures pointers to objects that other threads may still be accessing. In the second stage, the thread examines each node in its rlist and checks if it matches any entry in plist; nodes without a match are deemed unreferenced and eligible for reuse or deallocation, after which they are removed from rlist. The process relies on the atomicity of hazard pointer updates to guarantee that no thread can access a reclaimed node. The retirement and scanning processes are implemented via simple routines, as shown in the following pseudocode from the original formulation: RetireNode routine:

RetireNode(node) rlist = rlist → node // Insert node at the head of the retired list rcount = rcount + 1 // Increment the retired count if (rcount >= R) // If threshold R is reached Scan() // Scan hazard pointers

RetireNode(node) rlist = rlist → node // Insert node at the head of the retired list rcount = rcount + 1 // Increment the retired count if (rcount >= R) // If threshold R is reached Scan() // Scan hazard pointers

Scan routine:

Scan() Stage 1: plist = empty // Initialize private list of hazard pointers for each HP record hp in HP list if (hp != null) insert hp into plist // Add nonnull hazard pointer to plist Stage 2: for each node n in rlist if (lookup(n, plist) == failure) // If n not found in plist PrepareForReuse(n) // Reclaim node remove n from rlist // Remove node from retired list rcount = length(rlist) // Update retired count

Scan() Stage 1: plist = empty // Initialize private list of hazard pointers for each HP record hp in HP list if (hp != null) insert hp into plist // Add nonnull hazard pointer to plist Stage 2: for each node n in rlist if (lookup(n, plist) == failure) // If n not found in plist PrepareForReuse(n) // Reclaim node remove n from rlist // Remove node from retired list rcount = length(rlist) // Update retired count

These routines use a or sorted array for plist lookups to achieve efficient matching, with the PrepareForReuse step handling actual deallocation or .

Analysis

Time

The operations for setting and clearing hazard pointers are performed using (CAS) instructions, resulting in constant O(1) time per operation. This efficiency stems from the use of atomic reads and writes without requiring complex primitives beyond standard hardware support. The scanning process, which checks retired nodes against active hazard pointers to determine safe reclamation, has O(H) time complexity per scan, where H is the total number of hazard pointers in the system (typically bounded by the number of threads times the maximum hazard pointers per thread). Scans are triggered only when the number of retired nodes reaches a threshold R, set to Θ(H) to balance frequency and cost, leading to an amortized O(1) expected time per retired node across all retirements. Hazard pointers provide wait-free progress guarantees for the memory reclamation process, meaning each thread completes its reclamation operations in a bounded number of steps independent of the actions or delays of other threads. Performance is influenced by the number of threads, which directly scales H and thus the scan overhead, as well as the depth of the underlying , which affects the overall traversal time during which hazard pointers must be maintained.

Space bounds

Hazard pointers impose a modest per-thread space overhead, consisting of typically 1 to 2 hazard pointers for protecting objects and a local retired list that accumulates up to RR retired nodes before triggering a scan. This local list serves as a buffer for nodes pending reclamation, ensuring that threads manage their own retirements without immediate global coordination. Globally, the number of unreclaimed nodes is bounded by at most H+N×RH + N \times R, where HH denotes the total number of hazard pointers across all threads and NN is the number of threads; in practice, this yields a worst-case of O(H2)O(H^2). The retirement threshold RR is selected as R=H+HR = H + \sqrt{H}
Add your contribution
Related Hubs
User Avatar
No comments yet.