2525namespace unifex {
2626namespace _canary {
2727
28- // Two synchronization mechanisms:
28+ // Destructor arbitration uses tagged pointers (LSB = locked) on
29+ // both canary::watcher_ and watcher::canary_. Each destructor
30+ // locks its own pointer first, then accesses the other object.
31+ // Locking prevents the other destructor from completing (its CAS
32+ // on our pointer fails), ensuring the other object stays alive
33+ // while we access it.
2934//
30- // 1. CAS on canary::watcher_ (ownership arbitration):
31- // Both the canary destructor and watcher destructor try to CAS
32- // watcher_ from &watcher to nullptr. Exactly one wins:
33- // - Canary wins: it "owns" the watcher pointer and may access
34- // watcher members. The watcher destructor spins on canary_
35- // until the canary destructor signals completion.
36- // - Watcher wins: it clears the pointer. The canary destructor
37- // sees nullptr and does nothing.
35+ // Deadlock (both pointers locked) is resolved in favor of watcher:
36+ // canary unlocks watcher_ and spins until the watcher clears it.
3837//
39- // 2. Exchange on watcher::state_ (guard coordination ):
40- // alive(0) → guarded(1) by watcher::alive() [CAS]
41- // alive(0) → dead(2) by canary::~canary() [exchange]
42- // guarded(1) → dead(2) by canary::~canary() [exchange, then spinloop]
43- // dead(2) → done(3) by guard::~guard() [store, unblocks spinloop]
38+ // Guard coordination uses watcher::state_ (unchanged ):
39+ // alive(0) → guarded(1) by watcher::alive() [CAS]
40+ // alive(0) → dead(2) by canary::~canary() [exchange]
41+ // guarded(1) → dead(2) by canary::~canary() [exchange, then spinloop]
42+ // dead(2) → done(3) by guard::~guard() [store, unblocks spinloop]
4443
4544class canary {
4645 static constexpr uint8_t _alive = 0 ;
4746 static constexpr uint8_t _guarded = 1 ;
4847 static constexpr uint8_t _dead = 2 ;
4948 static constexpr uint8_t _done = 3 ;
5049
50+ static constexpr uintptr_t _lock_bit = 1 ;
51+
52+ template <typename T>
53+ static T* _lock (T* p) noexcept {
54+ return reinterpret_cast <T*>(reinterpret_cast <uintptr_t >(p) | _lock_bit);
55+ }
56+
57+ template <typename T>
58+ static T* _unlock (T* p) noexcept {
59+ return reinterpret_cast <T*>(reinterpret_cast <uintptr_t >(p) & ~_lock_bit);
60+ }
61+
62+ template <typename T>
63+ static bool _is_locked (T* p) noexcept {
64+ return reinterpret_cast <uintptr_t >(p) & _lock_bit;
65+ }
66+
5167public:
5268 class watcher ;
5369
@@ -73,9 +89,6 @@ class canary {
7389 std::atomic<uint8_t >* state_;
7490 };
7591
76- // Stack-local object that registers with the canary before a
77- // potentially-destroying call. Call alive() after the call to
78- // check whether the canary survived.
7992 class watcher {
8093 public:
8194 explicit watcher (canary& c) noexcept : canary_(&c) {
@@ -86,24 +99,41 @@ class canary {
8699 watcher& operator =(watcher&&) = delete ;
87100
88101 ~watcher () noexcept {
89- if (auto * c = canary_.load (std::memory_order_acquire)) {
90- watcher* expected = this ;
91- if (c->watcher_ .compare_exchange_strong (
92- expected, nullptr , std::memory_order_acq_rel)) {
93- // We cleared the pointer. Canary destructor will see nullptr.
94- } else {
95- // Canary destructor won the CAS — it holds a reference to
96- // us and will store nullptr to canary_ when done. Spin.
97- while (canary_.load (std::memory_order_acquire) != nullptr ) {
98- }
102+ // Step 1: lock our own pointer (canary_).
103+ auto * c = canary_.load (std::memory_order_relaxed);
104+ if (!c) {
105+ return ;
106+ }
107+ if (_is_locked (c) ||
108+ !canary_.compare_exchange_strong (
109+ c, _lock (c), std::memory_order_acq_rel)) {
110+ // Canary destructor locked or cleared canary_. It will
111+ // store nullptr when done. Spin.
112+ while (canary_.load (std::memory_order_acquire) != nullptr ) {
113+ }
114+ return ;
115+ }
116+ // canary_ is now locked. Canary destructor can't complete.
117+
118+ // Step 2: clear canary's watcher_ pointer.
119+ // Canary is alive (its destructor can't complete while our
120+ // canary_ is locked — its CAS on canary_ will fail).
121+ watcher* expected = this ;
122+ while (!c->watcher_ .compare_exchange_weak (
123+ expected, nullptr , std::memory_order_acq_rel)) {
124+ if (expected == nullptr ) {
125+ break ; // shouldn't happen, but handle gracefully
99126 }
127+ // watcher_ is locked (this|1) by canary destructor.
128+ // Canary will detect deadlock and unlock watcher_.
129+ // Spin-retry.
130+ expected = this ;
100131 }
132+
133+ // Step 3: unlock and clear canary_.
134+ canary_.store (nullptr , std::memory_order_release);
101135 }
102136
103- // If the canary is still alive, atomically transitions to guarded
104- // state and returns a truthy guard that blocks the canary's
105- // destructor. If the canary has been destroyed, returns a falsy
106- // guard.
107137 [[nodiscard]] guard alive () noexcept {
108138 uint8_t expected = _alive;
109139 if (state_.compare_exchange_strong (
@@ -119,46 +149,74 @@ class canary {
119149 std::atomic<uint8_t > state_{_alive};
120150 };
121151
152+ static_assert (
153+ alignof (watcher) >= 2,
154+ "watcher must be at least 2-byte aligned for LSB tagging");
155+
122156 canary () noexcept = default;
123157 canary (canary&&) = delete;
124158 canary& operator =(canary&&) = delete ;
125159
126- // Creates a watcher registered with this canary. The watcher is
127- // non-moveable and must be used as a stack-local variable.
128- // Returns a prvalue (C++17 guaranteed copy elision).
129160 [[nodiscard]] watcher watch () noexcept {
130161 UNIFEX_ASSERT (watcher_.load (std::memory_order_relaxed) == nullptr );
131162 return watcher{*this };
132163 }
133164
134165 ~canary () noexcept {
135- auto * w = watcher_.load (std::memory_order_acquire);
166+ // Step 1: lock our own pointer (watcher_).
167+ auto * w = watcher_.load (std::memory_order_relaxed);
136168 if (!w) {
137169 return ;
138170 }
139- // Try to claim ownership of the watcher pointer.
140- if ( !watcher_.compare_exchange_strong (
141- w, nullptr , std::memory_order_acq_rel)) {
142- // Watcher destructor won — it cleared the pointer . Done.
171+ if ( _is_locked (w) ||
172+ !watcher_.compare_exchange_strong (
173+ w, _lock (w) , std::memory_order_acq_rel)) {
174+ // Watcher destructor locked or cleared watcher_ . Done.
143175 return ;
144176 }
145- // We own w. The watcher destructor will spin on canary_ until
146- // we signal completion.
177+ // watcher_ is now locked. Watcher destructor can't complete.
178+
179+ // Step 2: lock watcher's canary_ pointer.
180+ // Watcher is alive (its destructor can't complete while our
181+ // watcher_ is locked — its CAS on watcher_ will fail).
182+ canary* expected = this ;
183+ if (!w->canary_ .compare_exchange_strong (
184+ expected, _lock (this ), std::memory_order_acq_rel)) {
185+ // canary_ is locked (this|1) by watcher destructor. Deadlock.
186+ // Canary yields: unlock watcher_ and let the watcher proceed.
187+ watcher_.store (w, std::memory_order_release); // unlock
188+ // Watcher will clear watcher_ to nullptr. Spin until done.
189+ while (watcher_.load (std::memory_order_acquire) != nullptr ) {
190+ }
191+ return ;
192+ }
193+ // Both pointers locked. We fully own the watcher.
194+
195+ // Step 3: guard coordination via state_.
147196 auto old = w->state_ .exchange (_dead, std::memory_order_acq_rel);
148197 if (old == _guarded) {
149- // Guard is held — spin until it is released.
150- while (w->state_ .load (std::memory_order_acquire) != _done) {
198+ // Guard is held — spin until released. The guard destructor
199+ // overwrites _dead with _done; we spin while state_ remains
200+ // _dead (the value we wrote) rather than waiting for a
201+ // specific successor value.
202+ while (w->state_ .load (std::memory_order_acquire) == _dead) {
151203 }
152204 }
153- // Signal the watcher destructor that we're done with its members.
205+
206+ // Step 4: signal completion and unlock.
154207 w->canary_ .store (nullptr , std::memory_order_release);
208+ watcher_.store (nullptr , std::memory_order_release);
155209 }
156210
157211private:
158212 friend class watcher ;
159213 std::atomic<watcher*> watcher_{nullptr };
160214};
161215
216+ static_assert (
217+ alignof (canary) >= 2,
218+ "canary must be at least 2-byte aligned for LSB tagging");
219+
162220} // namespace _canary
163221
164222using _canary::canary;
0 commit comments