[PATCH v2 2/3] libstdc++: Implement std::hazard_pointer (P2530R3)
Paul Xi Cao
paulxicao7@gmail.com
Mon Aug 17 22:26:58 GMT 2026
Implement the C++26 std::hazard_pointer facility per P2530R3
(working draft [saferecl.hp]).
Public API in namespace std:
* hazard_pointer_obj_base<T, D> -- CRTP base providing retire().
* hazard_pointer -- RAII handle to one hazard pointer in the default
domain; protect(), try_protect(), reset_protection(), swap().
* make_hazard_pointer() factory; free swap().
* __cpp_lib_hazard_pointer == 202306L (gated by the prior commit).
Implementation details (all in namespace std::__hazard_pointer):
* _Hazptr_obj -- non-template private base of
hazard_pointer_obj_base<T, D>. Carries the intrusive retire link,
the type-erased reclaim function, and the space P2530R3 sec. 1.5
reserves for the two extensions the paper expects (a cohort
pointer and a 64-bit counter). The reserved members are
initialised, not merely reserved: the constructor is inlined into
user code, so a later addition would be left uninitialised by
already-compiled callers. The corresponding check in retire() can
safely be added later, because nothing today can set a non-null
cohort.
* retire() is a pointer splice onto a per-thread intrusive list and
performs no allocation. [saferecl.hp.base] declares it noexcept,
so an allocating retire turns OOM into terminate().
* _Hazptr_rec -- one hazard pointer record, cache-line padded, in an
append-only list that is never unlinked. Claiming a record is a
test-and-test-and-set on a flag in the record and releasing one is
two plain stores, so neither path takes a lock; only appending a
record does, and that happens at most once per concurrently-live
hazard_pointer in the life of the process. _M_synchronize() walks
the list directly, with no lock and no snapshot array.
Never unlinking means a process that once peaked at N
concurrently-live handles walks N records for the rest of its
life; the intended follow-up is per-thread record caching
(Folly's hazptr_tc), which is ABI-neutral -- it lives in
thread-local storage inside the domain, so neither
hazard_pointer nor hazard_pointer_obj_base changes.
* hazard_pointer holds a _Hazptr_rec*, one word. P2530R3 sec. 1.5
item 3 puts the reserved domain pointer in the record rather than
in the handle, so that a future custom-domain extension never has
to widen a standard-specified type.
* The hazard and the scan key are both the _Hazptr_obj subobject
address, not the T*. The two differ whenever the base is not at
offset 0, e.g. struct T : Other, hazard_pointer_obj_base<T>, which
[saferecl.hp.general]/2 permits.
* _M_synchronize() executes a seq_cst atomic_thread_fence between
the collect step and the record scan. The fence is mandatory: the
acquire loads of the scan do not join the seq_cst total order the
reader side relies on, so without it a reader can re-validate its
source and keep dereferencing an object this scan has already
freed. Upgrading the scan's loads to seq_cst is not equivalent,
because the removal store on the source is user code that P2530R3
does not require to be seq_cst.
* Layout static_asserts pin sizeof/alignof of
hazard_pointer_obj_base and sizeof(hazard_pointer) on LP64.
hazard_pointer_obj_base is a standard-specified type that users
derive from, so its size is baked into user binaries and
doc/xml/manual/abi.xml lists changing it as a prohibited change.
The assertions are self-reporting: a failure reads
_Abi_pin<40, 32>, actual first.
Preconditions use __glibcxx_assert. Contracts integration is
deferred until libstdc++ exposes a stable "contracts actually
evaluated" macro distinct from __cpp_contracts; the "x is not
retired" precondition of [saferecl.hp.base]/6 is nevertheless checked
in every build, by the _M_next == this sentinel that the intrusive
list provides for free.
Changed since v1, all of it internal -- the public API is unchanged,
and so is the internal API the testsuite uses (_M_synchronize,
_M_active_slots, _M_retire_list_size):
* The seq_cst fence in _M_synchronize() (review point 1). This is a
use-after-free fix, not a tightening.
* The per-thread std::vector<_RetireRecord> retire list became the
intrusive list described above, which is what makes retire()
non-allocating (review point 2). This forced the layout of
hazard_pointer_obj_base to be frozen now rather than later; see
the reserved members above.
* The slot pool -- a std::deque<_Slot> with a parallel vector<bool>
free bitmap and a mutex over both -- became the record list, which
removed the std::find over the bitmap, the std::find_if in the
release path, and the mutex from both paths (review point 3).
* hazard_pointer_obj_base is no longer trivially copyable: reserving
anything makes the default constructor non-trivial, and the
not-retired sentinel needs user-provided copy and move, or
retiring a copy would look like a double retire.
* The offsetof(_M_deleter) == 0 static_assert is gone. The class is
no longer standard-layout, and the assertion was never needed --
the deleter is recovered by a real base-to-derived cast.
* The header is reformatted to libstdc++ house style.
libstdc++-v3/ChangeLog:
* include/bits/hazard_ptr.h: Replace stub with full
implementation of std::hazard_pointer,
std::hazard_pointer_obj_base, std::make_hazard_pointer, and
the supporting __hazard_pointer::_Domain.
---
libstdc++-v3/include/bits/hazard_ptr.h | 1056 ++++++++++++++++++++++++
1 file changed, 1056 insertions(+)
diff --git a/libstdc++-v3/include/bits/hazard_ptr.h b/libstdc++-v3/include/bits/hazard_ptr.h
index 3ffccfd2d19..1b4dc01c318 100644
--- a/libstdc++-v3/include/bits/hazard_ptr.h
+++ b/libstdc++-v3/include/bits/hazard_ptr.h
@@ -30,4 +30,1060 @@
#ifndef _GLIBCXX_HAZARD_PTR_H
#define _GLIBCXX_HAZARD_PTR_H 1
+#ifdef _GLIBCXX_SYSHDR
+#pragma GCC system_header
+#endif
+
+#include <atomic> // std::atomic, std::atomic_thread_fence
+#include <mutex> // std::mutex, std::lock_guard
+#include <vector> // std::vector
+#include <bits/move.h> // std::move, std::swap, std::__exchange
+#include <bits/stl_algo.h> // std::sort, std::binary_search
+#include <bits/unique_ptr.h> // std::default_delete
+#include <cstddef> // std::size_t
+#include <type_traits> // std::is_class_v, std::is_base_of_v
+
+namespace std _GLIBCXX_VISIBILITY(default)
+{
+_GLIBCXX_BEGIN_NAMESPACE_VERSION
+
+ template<typename _Tp, typename _Dp = default_delete<_Tp>>
+ class hazard_pointer_obj_base;
+
+ class hazard_pointer;
+
+ [[__nodiscard__]]
+ inline hazard_pointer make_hazard_pointer();
+
+ namespace __hazard_pointer
+ {
+ struct _Hazptr_obj;
+
+ // Type-erased reclamation: invokes the object's deleter on the object.
+ using _Reclaim_fn = void (*)(_Hazptr_obj*);
+
+ // Non-template private base of hazard_pointer_obj_base<T, D>.
+ //
+ // Carries the intrusive retire link and the erased reclaim function, so
+ // that retire() is a pointer splice into a per-thread list and cannot
+ // allocate. [saferecl.hp.base] declares retire() noexcept, so an
+ // allocating retire turns OOM into terminate(); putting the list storage
+ // in the object is the only way out, because a per-thread side table has
+ // to be grown by retire() itself.
+ //
+ // It also carries the space P2530R3 1.5 ("Guidance for ABI-Stability")
+ // reserves for the two extensions the paper expects. That reservation is
+ // a one-way door: hazard_pointer_obj_base is a standard-specified type
+ // that users *derive from*, so its size is baked into user binaries, and
+ // doc/xml/manual/abi.xml lists changing the layout of a standard-specified
+ // type as a prohibited change. The reserved members are initialised here
+ // for the same reason: the constructor is inlined into user code, so code
+ // compiled before a future extension would otherwise leave them
+ // uninitialised. The corresponding *check* in retire() can be added
+ // later, since nothing today can set a non-null cohort.
+ //
+ // Doubles as the tag the _Protectable concept detects: std::is_base_of
+ // ignores access, so one private base does both jobs.
+ struct _Hazptr_obj
+ {
+ // _M_next == this means "not retired"; every constructor re-establishes
+ // it. Backs the [saferecl.hp.base]/6 precondition "x is not retired" in
+ // every build rather than only under a debug mode, and costs nothing --
+ // the link has to be there anyway.
+ _Hazptr_obj* _M_next = this;
+ _Reclaim_fn _M_reclaim = nullptr;
+
+ // RESERVED for P2530R3 1.5. Never read or written by this
+ // implementation. Item 2 is cohort-based synchronous reclamation,
+ // item 1 integrated commutative counting; 64 bits because the reference
+ // implementation's counting extension uses a 64-bit word, and because
+ // with [[__no_unique_address__]] on the deleter it costs the same as 32.
+ void* _M_cohort = nullptr;
+ __UINT64_TYPE__ _M_count = 0;
+
+ [[__nodiscard__]]
+ bool
+ _M_not_retired() const noexcept
+ { return _M_next == this; }
+
+ _Hazptr_obj() noexcept = default;
+
+ // A copy is a new, unretired object, so the retirement state must not be
+ // copied -- otherwise retiring the copy would look like a double retire.
+ // This is what costs hazard_pointer_obj_base its trivial copyability,
+ // and there is no layout that is both trivially copyable and correct: a
+ // trivial copy necessarily copies the state.
+ _Hazptr_obj(const _Hazptr_obj&) noexcept { }
+ _Hazptr_obj(_Hazptr_obj&&) noexcept { }
+
+ // Assignment leaves the retirement state alone: assigning to an object
+ // does not retire or un-retire it. Deliberately a no-op, so
+ // self-assignment needs no special case.
+ _Hazptr_obj&
+ operator=(const _Hazptr_obj&) noexcept
+ { return *this; }
+
+ _Hazptr_obj&
+ operator=(_Hazptr_obj&&) noexcept
+ { return *this; }
+
+ ~_Hazptr_obj() = default;
+ };
+
+ // Intrusive singly-linked list of _Hazptr_obj, with O(1) splice.
+ //
+ // Head and tail rather than head alone: _M_synchronize() concatenates
+ // every thread's list into one chain, and without a tail pointer each
+ // concatenation would walk the list it is appending to.
+ //
+ // This is a correctness change, not a performance one, and measurement
+ // says so: against the pre-intrusive implementation, retire() alone is
+ // 11.1ns versus 11.2ns -- the per-thread _M_list_mutex costs more than the
+ // difference between a vector push_back and a pointer splice. What it
+ // buys is unconditional: [saferecl.hp.base] declares retire() noexcept,
+ // and an allocating retire turns OOM into terminate().
+ struct _Retire_list
+ {
+ _Hazptr_obj* _M_head = nullptr;
+ _Hazptr_obj* _M_tail = nullptr;
+ size_t _M_size = 0; // O(1); _M_retire_impl()'s threshold needs it
+
+ // Tests _M_head rather than _M_size: _M_head is the structural invariant
+ // the traversal in _M_synchronize() relies on, whereas _M_size is a
+ // cache maintained alongside it purely for the threshold. If the two
+ // ever disagree, believing _M_head fails safe.
+ [[__nodiscard__]]
+ bool
+ _M_empty() const noexcept
+ { return _M_head == nullptr; }
+
+ void
+ _M_push(_Hazptr_obj* __obj) noexcept
+ {
+ __obj->_M_next = _M_head;
+ if (_M_empty())
+ _M_tail = __obj;
+ _M_head = __obj;
+ ++_M_size;
+ }
+
+ // Move every node of __other to the front of *this; __other becomes
+ // empty. Order is irrelevant here -- the scan visits every node.
+ void
+ _M_splice(_Retire_list& __other) noexcept
+ {
+ if (__other._M_empty())
+ return;
+ if (_M_empty())
+ {
+ _M_head = __other._M_head;
+ _M_tail = __other._M_tail;
+ }
+ else
+ {
+ __other._M_tail->_M_next = _M_head;
+ _M_head = __other._M_head;
+ }
+ _M_size += __other._M_size;
+ __other._M_clear();
+ }
+
+ void
+ _M_clear() noexcept
+ {
+ _M_head = _M_tail = nullptr;
+ _M_size = 0;
+ }
+
+ // Detach the whole list, leaving *this empty.
+ [[__nodiscard__]]
+ _Retire_list
+ _M_take() noexcept
+ { return std::__exchange(*this, _Retire_list{}); }
+ };
+
+ // __GCC_DESTRUCTIVE_SIZE is not defined on targets that do not set the
+ // interference-size params; 64 is the value all such targets would use.
+#ifdef __GCC_DESTRUCTIVE_SIZE
+ inline constexpr size_t _S_cacheline_size = __GCC_DESTRUCTIVE_SIZE;
+#else
+ inline constexpr size_t _S_cacheline_size = 64;
+#endif
+
+ // One hazard pointer record, padded to a full cache line so that records
+ // owned by different threads do not share one.
+ //
+ // This is the "internal structure associated with the actual hazard
+ // pointers" of P2530R3 1.5 item 3. The paper is explicit that the domain
+ // pointer belongs here rather than in hazard_pointer, and gives the
+ // reason: keeping the handle one word wide is what buys the construction
+ // and destruction cost of its section 3.1.
+ //
+ // Records are never destroyed before the domain is, and are never
+ // unlinked. That is what lets _M_synchronize() walk the list with plain
+ // atomic loads and no lock, and what keeps the address a live
+ // hazard_pointer holds stable.
+ struct alignas(_S_cacheline_size) _Hazptr_rec
+ {
+ // The hazard pointer proper. Holds a _Hazptr_obj* (see
+ // reset_protection) or null.
+ atomic<void*> _M_hazard{nullptr};
+
+ // Claimed by a live hazard_pointer? Released with a plain store,
+ // claimed with a CAS, so neither path needs the allocation mutex.
+ atomic<bool> _M_active{false};
+
+ // Written once, before the record is published; read by every scan.
+ _Hazptr_rec* _M_next = nullptr;
+
+ // RESERVED for P2530R3 1.5 item 3 (custom domains). Null means the
+ // default domain, which is the representation the paper suggests, so a
+ // future ~hazard_pointer can test it without changing the layout again.
+ void* _M_domain = nullptr;
+ };
+
+ // Approximation of "hazard-protectable type" ([saferecl.hp.general]/2).
+ // Checks that _Tp is a class deriving from some hazard_pointer_obj_base
+ // <_Tp, _Dp> (for any _Dp), detected via the private _Hazptr_obj base
+ // (std::is_base_of ignores access). Not checked here, because _Dp is
+ // unknown -- retire(), where _Dp is explicit, checks them: that the base
+ // is public, that it is non-virtual, and that there is exactly one.
+ template<typename _Tp>
+ concept _Protectable = is_class_v<_Tp> && is_base_of_v<_Hazptr_obj, _Tp>;
+
+ } // namespace __hazard_pointer
+
+ // -------------------------------------------------------------------------
+ // hazard_pointer_obj_base
+ // -------------------------------------------------------------------------
+
+ /// Base class for objects protected by hazard pointers.
+ template<typename _Tp, typename _Dp>
+ class hazard_pointer_obj_base : private __hazard_pointer::_Hazptr_obj
+ {
+ public:
+ // Splices this object onto the calling thread's retire list in the
+ // default domain. Deletion is deferred until a scan confirms that no
+ // hazard pointer holds this address.
+ void retire(_Dp __d = _Dp()) noexcept;
+
+ protected:
+ hazard_pointer_obj_base() = default;
+ hazard_pointer_obj_base(const hazard_pointer_obj_base&) = default;
+ hazard_pointer_obj_base(hazard_pointer_obj_base&&) = default;
+ hazard_pointer_obj_base&
+ operator=(const hazard_pointer_obj_base&) = default;
+ hazard_pointer_obj_base&
+ operator=(hazard_pointer_obj_base&&) = default;
+ ~hazard_pointer_obj_base() = default;
+
+ private:
+ // [[__no_unique_address__]] is what pays for the space _Hazptr_obj
+ // reserves: with a stateless deleter (the default_delete case) it keeps
+ // the empty member out of the object's size instead of costing a full
+ // aligned word.
+ [[__no_unique_address__]] _Dp _M_deleter;
+
+ // reset_protection() has to map a _Tp* to the _Hazptr_obj subobject that
+ // the scan compares against, and that base is private. Private plus a
+ // friend keeps the implicit _Tp* -> _Hazptr_obj* conversion out of user
+ // overload resolution.
+ friend class hazard_pointer;
+ };
+
+ namespace __hazard_pointer
+ {
+ // Layout tripwire. hazard_pointer_obj_base is a standard-specified type
+ // that users derive from, so its size is baked into user binaries and
+ // cannot be changed once shipped -- doc/xml/manual/abi.xml lists that as a
+ // prohibited change. The reserved members exist precisely so the size
+ // does not have to move later; pin it here so an accidental change fails
+ // the build rather than the field. Guarded on 8-byte pointers so 32-bit
+ // targets are not held to a 64-bit number.
+ struct _Abi_probe : hazard_pointer_obj_base<_Abi_probe> { };
+
+ // Self-reporting, deliberately. A bare static_assert on sizeof() says
+ // only that the number moved, never what it moved to. Encoding both
+ // numbers as template arguments puts them in the diagnostic: the failure
+ // reads _Abi_pin<40, 32>, actual first.
+ template<size_t _Actual, size_t _Expected>
+ struct _Abi_pin
+ {
+ static_assert(sizeof(void*) != 8 || _Actual == _Expected,
+ "layout changed -- this is an ABI break, not a "
+ "refactor. The template arguments in this "
+ "diagnostic are <actual, expected>.");
+ static constexpr bool _S_ok = true;
+ };
+
+ static_assert(_Abi_pin<sizeof(hazard_pointer_obj_base<_Abi_probe>),
+ 32>::_S_ok);
+ static_assert(_Abi_pin<alignof(hazard_pointer_obj_base<_Abi_probe>),
+ 8>::_S_ok);
+ } // namespace __hazard_pointer
+
+ // -------------------------------------------------------------------------
+ // hazard_pointer
+ // -------------------------------------------------------------------------
+
+ /// RAII handle owning one hazard pointer in the default domain.
+ class hazard_pointer
+ {
+ public:
+ hazard_pointer() noexcept = default;
+
+ hazard_pointer(hazard_pointer&& __other) noexcept;
+
+ hazard_pointer&
+ operator=(hazard_pointer&& __other) noexcept;
+
+ ~hazard_pointer();
+
+ hazard_pointer(const hazard_pointer&) = delete;
+ hazard_pointer& operator=(const hazard_pointer&) = delete;
+
+ // True if this handle owns no hazard pointer. A handle that owns an
+ // unassociated hazard pointer (one holding nullptr) is NOT empty.
+ [[__nodiscard__]]
+ bool
+ empty() const noexcept;
+
+ template<typename _Tp>
+ [[__nodiscard__]]
+ _Tp*
+ protect(const atomic<_Tp*>& __src) noexcept;
+
+ template<typename _Tp>
+ [[__nodiscard__]]
+ bool
+ try_protect(_Tp*& __ptr, const atomic<_Tp*>& __src) noexcept;
+
+ template<typename _Tp>
+ void
+ reset_protection(const _Tp* __ptr) noexcept;
+
+ void
+ reset_protection(nullptr_t = nullptr) noexcept;
+
+ void
+ swap(hazard_pointer& __other) noexcept;
+
+ private:
+ // One word, and deliberately so: P2530R3 1.5 item 3 puts the reserved
+ // domain pointer in the record rather than here, because a future
+ // custom-domain extension must not have to grow hazard_pointer -- that
+ // would be another prohibited layout change on a standard-specified type.
+ // An index would have been the cheaper way to delete _M_release_rec()'s
+ // pool scan, but an index cannot name a domain, so it would have bought
+ // the scan back at the price of the extension.
+ __hazard_pointer::_Hazptr_rec* _M_rec = nullptr; // null = empty handle
+
+ explicit hazard_pointer(__hazard_pointer::_Hazptr_rec* __rec) noexcept;
+
+ friend hazard_pointer make_hazard_pointer();
+ };
+
+ namespace __hazard_pointer
+ {
+ // hazard_pointer is frozen for the same reason as
+ // hazard_pointer_obj_base, and pinned the same way. Staying one word is
+ // the whole point of P2530R3 1.5 item 3.
+ static_assert(_Abi_pin<sizeof(hazard_pointer), 8>::_S_ok,
+ "hazard_pointer must stay one word -- P2530R3 1.5 item 3");
+ } // namespace __hazard_pointer
+
+ // -------------------------------------------------------------------------
+ // __hazard_pointer::_Domain
+ // -------------------------------------------------------------------------
+
+ namespace __hazard_pointer
+ {
+ struct _RetireListNode;
+
+ // Owns the hazard pointer records and the pending retire lists.
+ // Process-global singleton; construction and destruction are restricted
+ // to _S_default_domain().
+ class _Domain
+ {
+ public:
+ // Claim an unused record, appending a new one if every existing record
+ // is taken. Throws std::bad_alloc only on OOM -- this is the one
+ // function on the path from make_hazard_pointer(), which
+ // [saferecl.hp.holder.ctor]/3 explicitly allows to throw. Every
+ // allocation this implementation performs on a live domain happens here.
+ [[__nodiscard__]]
+ _Hazptr_rec*
+ _M_acquire_rec();
+
+ // Return a record for reuse. Never allocates, never locks, never fails.
+ void
+ _M_release_rec(_Hazptr_rec* __rec) noexcept;
+
+ // Splice __obj onto the calling thread's retire list.
+ void
+ _M_retire_impl(_Hazptr_obj* __obj) noexcept;
+
+ // Reclaim every retired object whose address is not held by any hazard
+ // pointer. noexcept: reached from retire(), which
+ // [saferecl.hp.base] declares noexcept, and
+ // [saferecl.hp.general]/5 makes a throwing deleter undefined behaviour.
+ void
+ _M_synchronize() noexcept;
+
+ // Number of currently claimed records. Exposed for testing.
+ [[__nodiscard__]]
+ size_t
+ _M_active_slots() const noexcept;
+
+ // Number of entries in the calling thread's retire list; exposed for
+ // single-threaded testing.
+ [[__nodiscard__]]
+ size_t
+ _M_retire_list_size() const noexcept;
+
+ private:
+ // Head of the append-only record list. Published with release,
+ // traversed with acquire; records are never unlinked, so a traversal
+ // needs no lock and no snapshot array. The free flag lives in the
+ // record, which costs nothing because records are cache-line padded
+ // anyway, and it is what makes _M_release_rec() O(1).
+ atomic<_Hazptr_rec*> _M_recs_head{nullptr};
+
+ // Serialises appends only. The claim path is a CAS on
+ // _Hazptr_rec::_M_active and does not take it; the scan does not take
+ // it either.
+ mutex _M_rec_alloc_mutex;
+
+ atomic_size_t _M_active_count = 0; // records currently claimed
+ atomic_size_t _M_rec_count = 0; // records ever created
+
+ // Retired objects from threads that exited with still-protected
+ // survivors. Collected by _M_synchronize() alongside live-thread lists.
+ _Retire_list _M_orphan_list; // guarded by _M_orphan_mutex
+ mutex _M_orphan_mutex;
+
+ _RetireListNode* _M_retire_lists_head = nullptr;
+ size_t _M_retire_list_node_count = 0;
+ mutex _M_retire_lists_mutex; // guards the two members above
+
+ void
+ _M_ensure_node_registered();
+
+ void
+ _M_unregister_node(const _RetireListNode& __node) noexcept;
+
+ friend struct _RetireListNode;
+
+ // Private ctor/dtor plus a friend declaration enforces at compile time
+ // that the only _Domain instance is the static local in
+ // _S_default_domain(), so static storage duration is guaranteed by
+ // construction rather than by a runtime check. The friend declaration
+ // also covers the static-local destructor: the compiler registers the
+ // atexit handler from within the friend function's scope.
+ _Domain();
+ ~_Domain();
+ friend _Domain& _S_default_domain() noexcept;
+ };
+
+ // Process-wide default domain. Lazily initialised on first use.
+ inline _Domain&
+ _S_default_domain() noexcept
+ {
+ static _Domain __domain;
+ return __domain;
+ }
+
+ // Per-thread node in the retire-list registry. Each thread that calls
+ // _M_retire_impl() or _M_synchronize() has exactly one node, registered
+ // lazily on first use and removed when the thread exits.
+ //
+ // _M_list_mutex guards _M_list against concurrent access between
+ // _M_synchronize() (any thread) and _M_retire_impl() (the owning thread).
+ // _M_retire_lists_mutex guards the registry structure (the _M_next
+ // pointers and the head) but NOT the contents of _M_list.
+ struct _RetireListNode
+ {
+ _Retire_list _M_list; // guarded by _M_list_mutex
+ mutex _M_list_mutex;
+ _RetireListNode* _M_next = nullptr; // guarded by _M_retire_lists_mutex
+ bool _M_registered = false; // written once, by the owning thread
+
+ // Scratch for _M_synchronize()'s protected set, owned by and reused on
+ // this thread. Its capacity only has to grow when the record pool does,
+ // so after the first scan a steady-state reclamation allocates nothing.
+ //
+ // It cannot be sized from _M_acquire_rec() the way every other
+ // allocation is: a thread that only ever retires never calls
+ // make_hazard_pointer(), so it would never reach that path. Growth
+ // therefore happens inside a noexcept function and has to be able to
+ // fail -- see _M_synchronize().
+ vector<void*> _M_scan_buf;
+
+ // Called on thread exit. Drains the retire list, offloads survivors,
+ // then unregisters.
+ ~_RetireListNode();
+ };
+
+ inline thread_local _RetireListNode _S_tl_node;
+
+ } // namespace __hazard_pointer
+
+ // -------------------------------------------------------------------------
+ // Free functions
+ // -------------------------------------------------------------------------
+
+ inline void
+ swap(hazard_pointer& __a, hazard_pointer& __b) noexcept
+ { __a.swap(__b); }
+
+ // The only function in the public interface allowed to allocate, and the
+ // only one that does -- [saferecl.hp.holder.ctor]/3, "Throws: May throw
+ // bad_alloc".
+ [[__nodiscard__]]
+ inline hazard_pointer
+ make_hazard_pointer()
+ {
+ return hazard_pointer(__hazard_pointer::_S_default_domain()
+ ._M_acquire_rec());
+ }
+
+ // -------------------------------------------------------------------------
+ // hazard_pointer_obj_base implementation
+ // -------------------------------------------------------------------------
+
+ template<typename _Tp, typename _Dp>
+ inline void
+ hazard_pointer_obj_base<_Tp, _Dp>::retire(_Dp __d) noexcept
+ {
+ // noexcept per the standard API, and honoured on the allocation side:
+ // _M_retire_impl() splices this object onto an intrusive list, so there
+ // is no vector to grow and no way for OOM to reach std::terminate().
+ // The only operation left that can throw is mutex::lock (system_error),
+ // which is not a memory-pressure failure.
+ static_assert(is_invocable_v<_Dp, _Tp*>,
+ "D must be invocable with T* -- [saferecl.hp.base]/1");
+ static_assert(is_default_constructible_v<_Dp>,
+ "D must be default-constructible -- "
+ "[saferecl.hp.base]/3");
+ static_assert(is_move_assignable_v<_Dp>,
+ "D must be move-assignable -- [saferecl.hp.base]/3");
+ static_assert(is_base_of_v<hazard_pointer_obj_base<_Tp, _Dp>, _Tp>,
+ "T must derive from hazard_pointer_obj_base<T, D>"
+ " -- [saferecl.hp.base]/5");
+ static_assert(is_convertible_v<_Tp*,
+ hazard_pointer_obj_base<_Tp, _Dp>*>,
+ "hazard_pointer_obj_base<T, D> must be a public base of T"
+ " -- [saferecl.hp.base]/5");
+ static_assert(!is_virtual_base_of_v<hazard_pointer_obj_base<_Tp, _Dp>,
+ _Tp>,
+ "hazard_pointer_obj_base<T, D> must be a non-virtual base"
+ " of T -- [saferecl.hp.base]/5");
+
+ // [saferecl.hp.base]/6 precondition: x is not retired. The
+ // _M_next == this sentinel backs it in every build, unlike a bool member
+ // which would make sizeof(hazard_pointer_obj_base) depend on whether the
+ // translation unit was compiled with assertions enabled.
+ __glibcxx_assert(this->_M_not_retired());
+
+ _M_deleter = std::move(__d);
+
+ // Type erasure: the domain holds _Hazptr_obj*, and only this
+ // instantiation knows _Tp and _Dp. The cast to
+ // hazard_pointer_obj_base is a real base-to-derived cast, so it works at
+ // any offset -- no layout assumption is made.
+ this->_M_reclaim = [](__hazard_pointer::_Hazptr_obj* __p)
+ {
+ auto* const __base = static_cast<hazard_pointer_obj_base*>(__p);
+ _Tp* const __self = static_cast<_Tp*>(__base);
+ _Dp __d2 = std::move(__base->_M_deleter);
+ __d2(__self);
+ };
+
+ // Pass the _Hazptr_obj subobject, not the _Tp*: that is the address
+ // reset_protection() publishes and the scan compares against.
+ __hazard_pointer::_S_default_domain()._M_retire_impl(this);
+ }
+
+ // -------------------------------------------------------------------------
+ // hazard_pointer implementation
+ // -------------------------------------------------------------------------
+
+ inline
+ hazard_pointer::hazard_pointer(__hazard_pointer::_Hazptr_rec* __rec) noexcept
+ : _M_rec(__rec)
+ { }
+
+ // Two stores and no lock, which is what P2530R3 1.5 item 3 asks of this
+ // destructor: it calls it out by name as inlined and latency-critical.
+ inline
+ hazard_pointer::~hazard_pointer()
+ {
+ if (_M_rec)
+ {
+ reset_protection();
+ __hazard_pointer::_S_default_domain()._M_release_rec(_M_rec);
+ }
+ }
+
+ inline
+ hazard_pointer::hazard_pointer(hazard_pointer&& __other) noexcept
+ : _M_rec(__other._M_rec)
+ { __other._M_rec = nullptr; }
+
+ inline hazard_pointer&
+ hazard_pointer::operator=(hazard_pointer&& __other) noexcept
+ {
+ if (this == &__other)
+ return *this;
+ if (!empty())
+ {
+ reset_protection();
+ __hazard_pointer::_S_default_domain()._M_release_rec(_M_rec);
+ }
+ _M_rec = __other._M_rec;
+ __other._M_rec = nullptr;
+ return *this;
+ }
+
+ inline bool
+ hazard_pointer::empty() const noexcept
+ { return _M_rec == nullptr; }
+
+ template<typename _Tp>
+ inline bool
+ hazard_pointer::try_protect(_Tp*& __ptr,
+ const atomic<_Tp*>& __src) noexcept
+ {
+ static_assert(__hazard_pointer::_Protectable<_Tp>,
+ "T must be a hazard-protectable type"
+ " -- [saferecl.hp.holder.mem]/3");
+ __glibcxx_assert(!empty());
+ const _Tp* const __old = __ptr;
+ reset_protection(__old);
+ // seq_cst pairs with the seq_cst store in reset_protection(const T*) to
+ // form an SC pair: either the scan sees the hazard or this reload sees
+ // the new pointer. acquire alone is insufficient on weak-memory
+ // targets.
+ __ptr = __src.load(memory_order::seq_cst);
+ if (!(__old == __ptr)) // same expression as in the standard
+ reset_protection();
+ return __old == __ptr;
+ }
+
+ template<typename _Tp>
+ inline _Tp*
+ hazard_pointer::protect(const atomic<_Tp*>& __src) noexcept
+ {
+ static_assert(__hazard_pointer::_Protectable<_Tp>,
+ "T must be a hazard-protectable type"
+ " -- [saferecl.hp.holder.mem]/3");
+ // ABA safety comes from try_protect()'s seq_cst store and seq_cst
+ // reload, not from this loop; the loop only ensures convergence when
+ // __src changes.
+ _Tp* __ptr = __src.load(memory_order::relaxed);
+ while (!try_protect(__ptr, __src))
+ ;
+ return __ptr;
+ }
+
+ template<typename _Tp>
+ inline void
+ hazard_pointer::reset_protection(const _Tp* __ptr) noexcept
+ {
+ static_assert(__hazard_pointer::_Protectable<_Tp>,
+ "T must be a hazard-protectable type"
+ " -- [saferecl.hp.holder.mem]/7");
+ __glibcxx_assert(!empty());
+ if (__ptr == nullptr)
+ reset_protection();
+ else if (_M_rec)
+ {
+ // Publish the _Hazptr_obj subobject, not the _Tp*. That is what the
+ // scan compares against, and the two differ whenever the base is not
+ // at offset 0 -- e.g. struct T : Other, hazard_pointer_obj_base<T>,
+ // which [saferecl.hp.general]/2 permits. The offset is a
+ // compile-time constant, so the reader path pays an add at most.
+ const __hazard_pointer::_Hazptr_obj* const __obj = __ptr;
+ void* const __key
+ = const_cast<__hazard_pointer::_Hazptr_obj*>(__obj);
+ // seq_cst drains the store buffer so that the scan cannot miss the
+ // hazard; release is insufficient.
+ _M_rec->_M_hazard.store(__key, memory_order::seq_cst);
+ }
+ }
+
+ inline void
+ hazard_pointer::reset_protection(nullptr_t) noexcept
+ {
+ __glibcxx_assert(!empty());
+ if (_M_rec)
+ _M_rec->_M_hazard.store(nullptr, memory_order::release);
+ }
+
+ inline void
+ hazard_pointer::swap(hazard_pointer& __other) noexcept
+ { std::swap(_M_rec, __other._M_rec); }
+
+ // -------------------------------------------------------------------------
+ // __hazard_pointer::_Domain implementation
+ // -------------------------------------------------------------------------
+
+ namespace __hazard_pointer
+ {
+ // No pre-allocated pool: records are created on demand and never destroyed
+ // until the domain is, so a process that never uses hazard pointers pays
+ // nothing and the constructor cannot fail.
+ inline _Domain::_Domain() = default;
+
+ inline _Domain::~_Domain()
+ {
+ // Called during static-storage destruction. Thread-local storage is
+ // destroyed before static storage, so _S_tl_node is already gone and
+ // _M_synchronize() cannot be called. All threads have exited, so no
+ // hazard pointers are held and every orphan can be reclaimed
+ // unconditionally.
+ for (_Hazptr_obj* __obj = _M_orphan_list._M_head; __obj != nullptr;)
+ {
+ // Read before _M_reclaim(): it frees __obj.
+ _Hazptr_obj* const __next = __obj->_M_next;
+ __obj->_M_reclaim(__obj);
+ __obj = __next;
+ }
+ _M_orphan_list._M_clear();
+
+ // Records outlive every hazard_pointer by construction --
+ // _M_release_rec() only clears a flag -- so they are freed here, once
+ // all threads are gone.
+ for (const _Hazptr_rec* __rec = _M_recs_head.load(memory_order::relaxed);
+ __rec != nullptr;)
+ {
+ const _Hazptr_rec* const __next = __rec->_M_next;
+ delete __rec;
+ __rec = __next;
+ }
+ }
+
+ inline _Hazptr_rec*
+ _Domain::_M_acquire_rec()
+ {
+ // Fast path: claim an existing record with a CAS. No lock, so
+ // concurrent make_hazard_pointer() calls only contend when they race for
+ // the same record.
+ for (_Hazptr_rec* __rec = _M_recs_head.load(memory_order::acquire);
+ __rec != nullptr; __rec = __rec->_M_next)
+ {
+ // Test-and-test-and-set. The relaxed load is not an optimisation
+ // for the contended case, it is what stops the walk from being
+ // O(pool) *locked* RMWs: without it every occupied record on the way
+ // to a free one costs a failed compare_exchange, and a failed CAS
+ // still acquires the cache line exclusively and dirties it.
+ // Measured on x86_64, acquiring the 256th handle while 255 are held:
+ // 684ns with the bare CAS walk against 111ns without. A stale true
+ // only costs a skipped record, and a stale false is resolved by the
+ // CAS below.
+ if (__rec->_M_active.load(memory_order::relaxed))
+ continue;
+ bool __expected = false;
+ if (__rec->_M_active.compare_exchange_strong(__expected, true,
+ memory_order::acquire,
+ memory_order::relaxed))
+ {
+ // Relaxed: this counter feeds _M_retire_impl()'s heuristic
+ // threshold and _M_active_slots(). Nothing is ordered through
+ // it, and as a seq_cst RMW it was a second contended line on
+ // every acquire and release.
+ _M_active_count.fetch_add(1, memory_order::relaxed);
+ return __rec;
+ }
+ }
+
+ // Every record is taken -- append one. Serialised, but this is the rare
+ // path: it runs at most once per concurrently-live hazard_pointer, ever.
+ auto* const __rec = new _Hazptr_rec(); // throws bad_alloc on OOM
+ __rec->_M_active.store(true, memory_order::relaxed);
+ {
+ const lock_guard<mutex> __lk(_M_rec_alloc_mutex);
+ __rec->_M_next = _M_recs_head.load(memory_order::relaxed);
+ // Release, paired with the acquire loads above and in
+ // _M_synchronize(). A scan that does not observe this store cannot
+ // observe the hazard store that follows it either, and the seq_cst
+ // fence in _M_synchronize() already forces the scan to observe any
+ // hazard whose reader went on to validate. So a record published
+ // after a scan started is one whose reader has not yet committed to
+ // protecting anything.
+ _M_recs_head.store(__rec, memory_order::release);
+ }
+ _M_active_count.fetch_add(1, memory_order::relaxed);
+ _M_rec_count.fetch_add(1, memory_order::relaxed);
+ return __rec;
+ }
+
+ inline void
+ _Domain::_M_release_rec(_Hazptr_rec* __rec) noexcept
+ {
+ // Clear the hazard before offering the record for reuse.
+ __rec->_M_hazard.store(nullptr, memory_order::release);
+ _M_active_count.fetch_sub(1, memory_order::relaxed);
+ // Release, so that a thread which later claims this record via the
+ // acquiring CAS sees the cleared hazard.
+ __rec->_M_active.store(false, memory_order::release);
+ }
+
+ inline size_t
+ _Domain::_M_active_slots() const noexcept
+ { return _M_active_count.load(memory_order::relaxed); }
+
+ inline size_t
+ _Domain::_M_retire_list_size() const noexcept
+ { return _S_tl_node._M_list._M_size; }
+
+ inline void
+ _Domain::_M_retire_impl(_Hazptr_obj* __obj) noexcept
+ {
+ // Lazy registration: on the first call per thread, insert _S_tl_node
+ // into the registry. _M_registered is safe to check without a lock
+ // because only the owning thread ever writes it, and only once.
+ _M_ensure_node_registered();
+
+ // Splice under _M_list_mutex so that _M_synchronize() cannot observe a
+ // half-linked list. This is the operation that used to be a vector
+ // push_back, i.e. the reason retire() could turn OOM into terminate().
+ size_t __sz;
+ {
+ const lock_guard<mutex> __lk(_S_tl_node._M_list_mutex);
+ _S_tl_node._M_list._M_push(__obj);
+ __sz = _S_tl_node._M_list._M_size;
+ }
+
+ // Heuristic threshold: scan when the retire list grows to more than
+ // twice the number of active hazard pointers. Called with no lock held.
+ if (__sz > 2 * _M_active_count.load(memory_order::relaxed))
+ _M_synchronize();
+ }
+
+ // Called from ~_RetireListNode() when a thread exits. Splices the node
+ // out of the registry so _M_synchronize() never dereferences its
+ // soon-to-be-invalid list.
+ inline void
+ _Domain::_M_unregister_node(const _RetireListNode& __node) noexcept
+ {
+ const lock_guard<mutex> __lk(_M_retire_lists_mutex);
+ _RetireListNode* __curr = _M_retire_lists_head;
+ _RetireListNode* __prev = nullptr;
+ while (__curr && __curr != &__node)
+ {
+ __prev = __curr;
+ __curr = __curr->_M_next;
+ }
+ if (!__curr)
+ return; // node was never registered
+ if (__prev)
+ __prev->_M_next = __curr->_M_next;
+ else
+ _M_retire_lists_head = __curr->_M_next;
+ --_M_retire_list_node_count;
+ }
+
+ // Splice a collected-but-unscanned list onto the calling thread's own
+ // list, leaving it empty. Used both for survivors and for the bail-out
+ // path when the scan cannot be sized; in either case the objects stay
+ // retired and reachable, so a later scan reclaims them.
+ //
+ // "local" means the calling thread's, which is deliberately not the thread
+ // the objects were retired on -- that one may have exited since.
+ inline void
+ __splice_to_local_list(_Retire_list& __list) noexcept
+ {
+ if (__list._M_empty())
+ return;
+ const lock_guard<mutex> __lk(_S_tl_node._M_list_mutex);
+ _S_tl_node._M_list._M_splice(__list);
+ }
+
+ inline void
+ _Domain::_M_synchronize() noexcept
+ {
+ // Ensure this thread is registered so survivors have a valid home to
+ // return to.
+ _M_ensure_node_registered();
+
+ // Step 1: collect -- detach every thread's retire list, then the orphan
+ // list, and concatenate them. Done BEFORE the scan so that a reader
+ // publishing a hazard concurrently with the collect either sees the
+ // object still in its source (and thus retries) or has the hazard
+ // visible to the scan below. Every step here is a pointer splice: the
+ // collect phase does not allocate.
+ _Retire_list __pending;
+ {
+ const lock_guard<mutex> __lk(_M_retire_lists_mutex);
+ for (_RetireListNode* __n = _M_retire_lists_head; __n;
+ __n = __n->_M_next)
+ {
+ // The splice touches only the local __pending, so it does not
+ // belong inside _M_list_mutex -- which is the lock
+ // _M_retire_impl() takes on the owning thread's hot path.
+ _Retire_list __taken;
+ {
+ const lock_guard<mutex> __lk2(__n->_M_list_mutex);
+ __taken = __n->_M_list._M_take();
+ }
+ __pending._M_splice(__taken);
+ }
+ }
+ {
+ _Retire_list __taken;
+ {
+ const lock_guard<mutex> __lk(_M_orphan_mutex);
+ __taken = _M_orphan_list._M_take();
+ }
+ __pending._M_splice(__taken);
+ }
+ if (__pending._M_empty())
+ return;
+
+ // Step 2: size the protected-set buffer. The record list needs no
+ // snapshot of its own: records are never unlinked, so the scan below
+ // walks it directly, with no lock and no intermediate array.
+ //
+ // The buffer belongs to this thread and is reused, so it only has to
+ // grow when the record pool does. Growth still has to be able to fail,
+ // because a thread that only retires never calls make_hazard_pointer()
+ // and so can first reach this point inside a noexcept function.
+ //
+ // On failure the scan falls back to a linear membership test instead of
+ // giving up: reclamation always completes, it is just slower. Each
+ // hazard is then re-loaded once per candidate rather than once in total,
+ // which is sound -- every load still happens after the fence below, and
+ // correctness needs each load to be ordered after it, not to be part of
+ // one instant.
+ vector<void*>& __snapshot = _S_tl_node._M_scan_buf;
+ __snapshot.clear();
+ bool __have_buffer = true;
+ __try
+ {
+ __snapshot.reserve(_M_rec_count.load(memory_order::relaxed));
+ }
+ __catch(...)
+ {
+ __have_buffer = false;
+ }
+
+ // Reclaim-side fence -- MANDATORY, not an optimisation barrier.
+ //
+ // The acquire loads below do not join the seq_cst total order that the
+ // reader side relies on, and the collect step's lock chain only orders a
+ // *writer's* retirement against the collect: it adds no edge between an
+ // independent reader's hazard store and this scan. Without this fence a
+ // reader can re-validate its source, still see the object (so it keeps
+ // dereferencing it), while this scan reads a stale empty record and
+ // frees it. [saferecl.hp.general]/6 requires the end of the protection
+ // epoch to strongly happen before the reclamation, which an
+ // acquire-only scan does not provide.
+ //
+ // Upgrading the loads below to seq_cst instead of fencing does NOT fix
+ // it: the removal store on the source is user code, and P2530R3 does not
+ // require it to be seq_cst, so the StoreLoad reordering survives.
+ //
+ // Found in review by Thomas Rodgers, confirmed with Maged Michael, and
+ // observed on POWER9/POWER10 hardware.
+ atomic_thread_fence(memory_order::seq_cst);
+
+ _Hazptr_rec* const __recs = _M_recs_head.load(memory_order::acquire);
+
+ if (__have_buffer)
+ {
+ for (const _Hazptr_rec* __rec = __recs; __rec != nullptr;
+ __rec = __rec->_M_next)
+ if (void* const __p = __rec->_M_hazard.load(memory_order::acquire))
+ __snapshot.push_back(__p); // within the reserved capacity
+ std::sort(__snapshot.begin(), __snapshot.end());
+ }
+
+ // Step 3: reclaim every object not in the protected set. No lock is
+ // held here, so deleters may safely call retire() or synchronize().
+ //
+ // Both sides of the comparison are _Hazptr_obj subobject addresses:
+ // retire() passes one, and reset_protection() publishes one.
+ const auto __protected = [&](_Hazptr_obj* __obj) noexcept
+ {
+ void* const __key = __obj;
+ if (__have_buffer)
+ return std::binary_search(__snapshot.begin(), __snapshot.end(),
+ __key);
+ for (const _Hazptr_rec* __rec = __recs; __rec != nullptr;
+ __rec = __rec->_M_next)
+ if (__rec->_M_hazard.load(memory_order::acquire) == __key)
+ return true;
+ return false;
+ };
+
+ _Retire_list __survivors;
+ for (_Hazptr_obj* __obj = __pending._M_head; __obj != nullptr;)
+ {
+ // Read before the object is spliced or freed.
+ _Hazptr_obj* const __next = __obj->_M_next;
+ if (__protected(__obj))
+ __survivors._M_push(__obj);
+ else
+ __obj->_M_reclaim(__obj);
+ __obj = __next;
+ }
+
+ // Step 4: put survivors back into the calling thread's own list. They
+ // cannot be returned to their original threads, which may have exited
+ // between the collect and here; the calling thread's list is guaranteed
+ // alive for the duration of this call.
+ __splice_to_local_list(__survivors);
+ }
+
+ inline void
+ _Domain::_M_ensure_node_registered()
+ {
+ if (_S_tl_node._M_registered)
+ return;
+ {
+ const lock_guard<mutex> __lk(_M_retire_lists_mutex);
+ _S_tl_node._M_next = _M_retire_lists_head;
+ _M_retire_lists_head = &_S_tl_node;
+ ++_M_retire_list_node_count;
+ }
+ _S_tl_node._M_registered = true; // only after successful registration
+ }
+
+ inline
+ _RetireListNode::~_RetireListNode()
+ {
+ if (!_M_registered)
+ return; // _M_ensure_node_registered() never ran -- nothing to do
+
+ _Domain& __domain = _S_default_domain();
+
+ // Reclaim as much as possible before this thread's retire list goes
+ // away. _M_synchronize() puts survivors back into _M_list, which is a
+ // member of *this and therefore still alive throughout this destructor.
+ __domain._M_synchronize();
+
+ // Unregister before touching _M_list without _M_list_mutex.
+ // _M_unregister_node() acquires _M_retire_lists_mutex, which
+ // _M_synchronize() holds for its entire collect loop -- so this call
+ // blocks until any concurrent _M_synchronize() that has already seen
+ // this node has fully released both mutexes. After it returns, no
+ // future _M_synchronize() can find this node.
+ __domain._M_unregister_node(*this);
+
+ // Move any remaining survivors (still actively protected) to the
+ // domain's orphan list so a future scan from any thread can reclaim
+ // them.
+ if (!_M_list._M_empty())
+ {
+ const lock_guard<mutex> __lk(__domain._M_orphan_mutex);
+ __domain._M_orphan_list._M_splice(_M_list);
+ }
+ }
+
+ } // namespace __hazard_pointer
+
+_GLIBCXX_END_NAMESPACE_VERSION
+} // namespace std
+
#endif // _GLIBCXX_HAZARD_PTR_H
--
2.54.0
More information about the Libstdc++
mailing list