Remove the unnecessary __wait_args_base base class, which seems to serve no purpose. Everywhere it's used we actually want __wait_args instead.
The code using the __wait_flags bitmask type is broken, because the __spin_only constant includes the __do_spin element. This means that testing (__args & __wait_flags::__spin_only) will be inadvertently true when only __do_spin is set. This causes the __wait_until_impl function to never actually wait on the futex (or condition variable), turning all uses of that function into expensive busy spins. Change __spin_only to be a single bit (i.e. a bitmask element) and adjust the places where that bit is set so that they also use the __do_spin element. Update the __args._M_old value when looping in __atomic_wait_address, so that the next wait doesn't fail spuriously. With the new __atomic_wait_address logic, the value function needs to return the correct type, not just a bool. Without that change, the boolean value returned by the value function is used as the value passed to the futex wait, but that mean we're comparing (_M_a == 0) to _M_a and so can block on the futex when we shouldn't, and then never wake up. libstdc++-v3/ChangeLog: * include/bits/atomic_timed_wait.h (__cond_wait_impl): Add missing inline keyword. (__spin_until_impl): Change parameter from __wait_args_base* to __wait_args*. Replace make_pair with list-initialization. Initialize variable for return value. (__wait_until_impl): Likewise. Remove some preprocessor conditional logic. Use _M_tracker() for contention tracking. Avoid unnecessary const_cast. (__wait_until): Replace make_pair with list-initialization. (__wait_for): Change parameter from __wait_args_base* to __wait_args*. Add __do_spin flag to args. * include/bits/atomic_wait.h (__wait_flags): Do not set the __do_spin flag in the __spin_only enumerator. Comment out the unused __abi_version_mask enumerator. Define operator| and operator|= overloads. (__wait_args_base): Remove. (__wait_args): Move __wait_args_base members here and remove the base class. Constrain constructor to avoid accidents. (__wait_args::_M_tracker): Helper function to create an RAII object to track contention. (__spin_impl): Change parameter from __wait_args_base* to __wait_args*. Replace make_pair call with list-initialization. (__wait_impl): Likewise. Remove some preprocessor conditional logic. Always store old value in __args._M_old. Avoid unnecessary const_cast. Use _M_tracker. (__notify_impl): Change parameter to __wait_args*. Remove some preprocessor conditional logic. (__atomic_wait_address): Add comment. Update __args._M_old on each iteration. (__atomic_wait_address_v): Add comment. * include/std/latch (latch::wait): Adjust predicates for new logic. * testsuite/29_atomics/atomic_integral/wait_notify.cc: Improve test. --- libstdc++-v3/include/bits/atomic_timed_wait.h | 73 ++---- libstdc++-v3/include/bits/atomic_wait.h | 221 +++++++++--------- libstdc++-v3/include/std/latch | 8 +- .../29_atomics/atomic_integral/wait_notify.cc | 4 + 4 files changed, 147 insertions(+), 159 deletions(-) diff --git a/libstdc++-v3/include/bits/atomic_timed_wait.h b/libstdc++-v3/include/bits/atomic_timed_wait.h index 73acea939504..7e2017f2f515 100644 --- a/libstdc++-v3/include/bits/atomic_timed_wait.h +++ b/libstdc++-v3/include/bits/atomic_timed_wait.h @@ -111,7 +111,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION #ifdef _GLIBCXX_HAS_GTHREADS // Returns true if wait ended before timeout. - bool + inline bool __cond_wait_until(__condvar& __cv, mutex& __mx, const __wait_clock_t::time_point& __atime) { @@ -136,14 +136,14 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION inline __wait_result_type __spin_until_impl(const __platform_wait_t* __addr, - const __wait_args_base* __a, + const __wait_args* __a, const __wait_clock_t::time_point& __deadline) { __wait_args __args{ *__a }; auto __t0 = __wait_clock_t::now(); using namespace literals::chrono_literals; - __platform_wait_t __val; + __platform_wait_t __val{}; auto __now = __wait_clock_t::now(); for (; __now < __deadline; __now = __wait_clock_t::now()) { @@ -157,44 +157,32 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION #endif if (__elapsed > 4us) __thread_yield(); - else - { - auto __res = __detail::__spin_impl(__addr, __a); - if (__res.first) - return __res; - } + else if (auto __res = __detail::__spin_impl(__addr, __a); __res.first) + return __res; __atomic_load(__addr, &__val, __args._M_order); if (__val != __args._M_old) - return make_pair(true, __val); + return { true, __val }; } - return make_pair(false, __val); + return { false, __val }; } inline __wait_result_type __wait_until_impl(const __platform_wait_t* __addr, - const __wait_args_base* __a, + const __wait_args* __a, const __wait_clock_t::time_point& __atime) { __wait_args __args{ *__a }; -#ifdef _GLIBCXX_HAVE_PLATFORM_TIMED_WAIT __waiter_pool_impl* __pool = nullptr; -#else - // if we don't have __platform_wait, we always need the side-table - __waiter_pool_impl* __pool = &__waiter_pool_impl::_S_impl_for(__addr); -#endif - - __platform_wait_t* __wait_addr; + const __platform_wait_t* __wait_addr; if (__args & __wait_flags::__proxy_wait) { -#ifdef _GLIBCXX_HAVE_PLATFORM_TIMED_WAIT __pool = &__waiter_pool_impl::_S_impl_for(__addr); -#endif __wait_addr = &__pool->_M_ver; __atomic_load(__wait_addr, &__args._M_old, __args._M_order); } else - __wait_addr = const_cast<__platform_wait_t*>(__addr); + __wait_addr = __addr; if (__args & __wait_flags::__do_spin) { @@ -205,43 +193,31 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION return __res; } - if (!(__args & __wait_flags::__track_contention)) - { - // caller does not externally track contention -#ifdef _GLIBCXX_HAVE_PLATFORM_TIMED_WAIT - __pool = (__pool == nullptr) ? &__waiter_pool_impl::_S_impl_for(__addr) - : __pool; -#endif - __pool->_M_enter_wait(); - } + auto __tracker = __args._M_tracker(__pool, __addr); - __wait_result_type __res; #ifdef _GLIBCXX_HAVE_PLATFORM_TIMED_WAIT if (__platform_wait_until(__wait_addr, __args._M_old, __atime)) - __res = make_pair(true, __args._M_old); + return { true, __args._M_old }; else - __res = make_pair(false, __args._M_old); + return { false, __args._M_old }; #else - __platform_wait_t __val; + __platform_wait_t __val{}; __atomic_load(__wait_addr, &__val, __args._M_order); if (__val == __args._M_old) { + if (!__pool) + __pool = &__waiter_pool_impl::_S_impl_for(__addr); lock_guard<mutex> __l{ __pool->_M_mtx }; __atomic_load(__wait_addr, &__val, __args._M_order); - if (__val == __args._M_old && - __cond_wait_until(__pool->_M_cv, __pool->_M_mtx, __atime)) - __res = make_pair(true, __val); + if (__val == __args._M_old + && __cond_wait_until(__pool->_M_cv, __pool->_M_mtx, __atime)) + return { true, __val }; } - else - __res = make_pair(false, __val); + return { false, __val }; #endif - - if (!(__args & __wait_flags::__track_contention)) - // caller does not externally track contention - __pool->_M_leave_wait(); - return __res; } + // Returns {true, val} if wait ended before a timeout. template<typename _Clock, typename _Dur> __wait_result_type __wait_until(const __platform_wait_t* __addr, const __wait_args* __args, @@ -259,22 +235,23 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION // we need to check against the caller-supplied clock // to tell whether we should return a timeout. if (_Clock::now() < __atime) - return make_pair(true, __res.second); + __res.first = true; } return __res; } } + // Returns {true, val} if wait ended before a timeout. template<typename _Rep, typename _Period> __wait_result_type - __wait_for(const __platform_wait_t* __addr, const __wait_args_base* __a, + __wait_for(const __platform_wait_t* __addr, const __wait_args* __a, const chrono::duration<_Rep, _Period>& __rtime) noexcept { __wait_args __args{ *__a }; if (!__rtime.count()) { // no rtime supplied, just spin a bit - __args |= __wait_flags::__spin_only; + __args |= __wait_flags::__do_spin | __wait_flags::__spin_only; return __detail::__wait_impl(__addr, &__args); } diff --git a/libstdc++-v3/include/bits/atomic_wait.h b/libstdc++-v3/include/bits/atomic_wait.h index ebab4b099e66..29b83cad6e6c 100644 --- a/libstdc++-v3/include/bits/atomic_wait.h +++ b/libstdc++-v3/include/bits/atomic_wait.h @@ -177,6 +177,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION #ifndef _GLIBCXX_HAVE_PLATFORM_WAIT __condvar _M_cv; #endif + __waiter_pool_impl() = default; void @@ -211,88 +212,104 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION __proxy_wait = 1, __track_contention = 2, __do_spin = 4, - __spin_only = 8 | __do_spin, // implies __do_spin - __abi_version_mask = 0xffff0000, + __spin_only = 8, // Ignored unless __do_spin is also set. + // __abi_version_mask = 0xffff0000, }; - struct __wait_args_base + [[__gnu__::__always_inline__]] + constexpr __wait_flags + operator|(__wait_flags __l, __wait_flags __r) noexcept + { + using _Ut = underlying_type_t<__wait_flags>; + return static_cast<__wait_flags>(static_cast<_Ut>(__l) + | static_cast<_Ut>(__r)); + } + + [[__gnu__::__always_inline__]] + constexpr __wait_flags& + operator|=(__wait_flags& __l, __wait_flags __r) noexcept + { return __l = __l | __r; } + + struct __wait_args { __wait_flags _M_flags; int _M_order = __ATOMIC_ACQUIRE; __platform_wait_t _M_old = 0; - }; - struct __wait_args : __wait_args_base - { - template<typename _Tp> - explicit __wait_args(const _Tp* __addr, - bool __bare_wait = false) noexcept - : __wait_args_base{ _S_flags_for(__addr, __bare_wait) } + template<typename _Tp> requires (!is_same_v<_Tp, __wait_args>) + explicit + __wait_args(const _Tp* __addr, bool __bare_wait = false) noexcept + : _M_flags{ _S_flags_for(__addr, __bare_wait) } { } __wait_args(const __platform_wait_t* __addr, __platform_wait_t __old, int __order, bool __bare_wait = false) noexcept - : __wait_args_base{ _S_flags_for(__addr, __bare_wait), __order, __old } - { } - - explicit __wait_args(const __wait_args_base& __base) - : __wait_args_base{ __base } - { } + : _M_flags{ _S_flags_for(__addr, __bare_wait)}, + _M_order{__order}, _M_old{__old} + { } __wait_args(const __wait_args&) noexcept = default; - __wait_args& - operator=(const __wait_args&) noexcept = default; + __wait_args& operator=(const __wait_args&) noexcept = default; + // Test whether _M_flags & __flag is non-zero. bool - operator&(__wait_flags __flag) const noexcept + operator&(__wait_flags __flags) const noexcept { - using __t = underlying_type_t<__wait_flags>; - return static_cast<__t>(_M_flags) - & static_cast<__t>(__flag); - } - - __wait_args - operator|(__wait_flags __flag) const noexcept - { - using __t = underlying_type_t<__wait_flags>; - __wait_args __res{ *this }; - const auto __flags = static_cast<__t>(__res._M_flags) - | static_cast<__t>(__flag); - __res._M_flags = __wait_flags{ __flags }; - return __res; + using _Ut = underlying_type_t<__wait_flags>; + return static_cast<_Ut>(_M_flags) & static_cast<_Ut>(__flags); } + // Set __flags in _M_flags. __wait_args& - operator|=(__wait_flags __flag) noexcept + operator|=(__wait_flags __flags) noexcept { - using __t = underlying_type_t<__wait_flags>; - const auto __flags = static_cast<__t>(_M_flags) - | static_cast<__t>(__flag); - _M_flags = __wait_flags{ __flags }; + _M_flags |= __flags; return *this; } - private: - static int - constexpr _S_default_flags() noexcept + // Return an RAII type that calls __pool->_M_leave_wait() on destruction. + auto + _M_tracker(__waiter_pool_impl* __pool, const void* __addr) const { - using __t = underlying_type_t<__wait_flags>; - return static_cast<__t>(__wait_flags::__abi_version) - | static_cast<__t>(__wait_flags::__do_spin); + struct _Guard + { + explicit _Guard(__waiter_pool_impl* __p) : _M_pool(__p) { } + _Guard(const _Guard&) = delete; + ~_Guard() { if (_M_pool) _M_pool->_M_leave_wait(); } + __waiter_pool_impl* _M_pool; + }; + + if (*this & __wait_flags::__track_contention) + { + // Caller does not externally track contention, + // so we want to increment+decrement __pool->_M_waiters + + // First make sure we have a pool for the address. + if (!__pool) + __pool = &__waiter_pool_impl::_S_impl_for(__addr); + // Increment number of waiters: + __pool->_M_enter_wait(); + // Returned _Guard will decrement it again on destruction. + return _Guard{__pool}; + } + return _Guard{nullptr}; // For bare waits caller tracks waiters. } + private: template<typename _Tp> - static __wait_flags - constexpr _S_flags_for(const _Tp*, bool __bare_wait) noexcept + static constexpr __wait_flags + _S_flags_for(const _Tp*, bool __bare_wait) noexcept { - auto __res = _S_default_flags(); + using enum __wait_flags; + __wait_flags __res = __abi_version | __do_spin; if (!__bare_wait) - __res |= static_cast<int>(__wait_flags::__track_contention); + __res |= __track_contention; if constexpr (!__platform_wait_uses_type<_Tp>) - __res |= static_cast<int>(__wait_flags::__proxy_wait); - return static_cast<__wait_flags>(__res); + __res |= __proxy_wait; + return __res; } + // XXX what is this for? It's never used. template<typename _Tp> static int _S_memory_order_for(const _Tp*, int __order) noexcept @@ -304,50 +321,39 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION }; using __wait_result_type = pair<bool, __platform_wait_t>; + inline __wait_result_type - __spin_impl(const __platform_wait_t* __addr, const __wait_args_base* __args) + __spin_impl(const __platform_wait_t* __addr, const __wait_args* __args) { __platform_wait_t __val; for (auto __i = 0; __i < __atomic_spin_count; ++__i) { __atomic_load(__addr, &__val, __args->_M_order); if (__val != __args->_M_old) - return make_pair(true, __val); + return { true, __val }; if (__i < __atomic_spin_count_relax) __detail::__thread_relax(); else __detail::__thread_yield(); } - return make_pair(false, __val); + return { false, __val }; } inline __wait_result_type - __wait_impl(const __platform_wait_t* __addr, const __wait_args_base* __a) + __wait_impl(const __platform_wait_t* __addr, const __wait_args* __a) { __wait_args __args{ *__a }; -#ifdef _GLIBCXX_HAVE_PLATFORM_WAIT __waiter_pool_impl* __pool = nullptr; -#else - // if we don't have __platform_wait, we always need the side-table - __waiter_pool_impl* __pool = &__waiter_pool_impl::_S_impl_for(__addr); -#endif - __platform_wait_t* __wait_addr; - __platform_wait_t __old; + const __platform_wait_t* __wait_addr; if (__args & __wait_flags::__proxy_wait) { -#ifdef _GLIBCXX_HAVE_PLATFORM_WAIT __pool = &__waiter_pool_impl::_S_impl_for(__addr); -#endif __wait_addr = &__pool->_M_ver; - __atomic_load(__wait_addr, &__old, __args._M_order); + __atomic_load(__wait_addr, &__args._M_old, __args._M_order); } else - { - __wait_addr = const_cast<__platform_wait_t*>(__addr); - __old = __args._M_old; - } - + __wait_addr = __addr; if (__args & __wait_flags::__do_spin) { @@ -358,86 +364,75 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION return __res; } - if (!(__args & __wait_flags::__track_contention)) - { - // caller does not externally track contention -#ifdef _GLIBCXX_HAVE_PLATFORM_WAIT - __pool = (__pool == nullptr) ? &__waiter_pool_impl::_S_impl_for(__addr) - : __pool; -#endif - __pool->_M_enter_wait(); - } + auto __tracker = __args._M_tracker(__pool, __addr); - __wait_result_type __res; #ifdef _GLIBCXX_HAVE_PLATFORM_WAIT __platform_wait(__wait_addr, __args._M_old); - __res = make_pair(false, __args._M_old); + return { false, __args._M_old }; #else __platform_wait_t __val; __atomic_load(__wait_addr, &__val, __args._M_order); if (__val == __args._M_old) { + if (!__pool) + __pool = &__waiter_pool_impl::_S_impl_for(__addr); lock_guard<mutex> __l{ __pool->_M_mtx }; __atomic_load(__wait_addr, &__val, __args._M_order); if (__val == __args._M_old) __pool->_M_cv.wait(__pool->_M_mtx); } - __res = make_pair(false, __val); + return { false, __val }; #endif - - if (!(__args & __wait_flags::__track_contention)) - // caller does not externally track contention - __pool->_M_leave_wait(); - return __res; } inline void __notify_impl(const __platform_wait_t* __addr, [[maybe_unused]] bool __all, - const __wait_args_base* __a) + const __wait_args* __a) { - __wait_args __args{ __a }; -#ifdef _GLIBCXX_HAVE_PLATFORM_WAIT + __wait_args __args{ *__a }; __waiter_pool_impl* __pool = nullptr; -#else - // if we don't have __platform_notify, we always need the side-table - __waiter_pool_impl* __pool = &__waiter_pool_impl::_S_impl_for(__addr); -#endif - if (!(__args & __wait_flags::__track_contention)) + if (__args & __wait_flags::__track_contention) { -#ifdef _GLIBCXX_HAVE_PLATFORM_WAIT __pool = &__waiter_pool_impl::_S_impl_for(__addr); -#endif if (!__pool->_M_waiting()) return; } - __platform_wait_t* __wait_addr; + const __platform_wait_t* __wait_addr; if (__args & __wait_flags::__proxy_wait) { -#ifdef _GLIBCXX_HAVE_PLATFORM_WAIT - __pool = (__pool == nullptr) ? &__waiter_pool_impl::_S_impl_for(__addr) - : __pool; -#endif - __wait_addr = &__pool->_M_ver; - __atomic_fetch_add(__wait_addr, 1, __ATOMIC_RELAXED); - __all = true; - } + if (!__pool) + __pool = &__waiter_pool_impl::_S_impl_for(__addr); + // Waiting for *__addr is actually done on the proxy's _M_ver. + __wait_addr = &__pool->_M_ver; + __atomic_fetch_add(&__pool->_M_ver, 1, __ATOMIC_RELAXED); + // Because the proxy might be shared by several waiters waiting + // on different atomic variables, we need to wake them all so + // they can re-evaluate their conditions to see if they should + // stop waiting or should wait again. + __all = true; + } + else // Use the atomic variable's own address. + __wait_addr = __addr; #ifdef _GLIBCXX_HAVE_PLATFORM_WAIT __platform_notify(__wait_addr, __all); #else + if (!__pool) + __pool = &__waiter_pool_impl::_S_impl_for(__addr); lock_guard<mutex> __l{ __pool->_M_mtx }; __pool->_M_cv.notify_all(); #endif } } // namespace __detail - template<typename _Tp, - typename _Pred, typename _ValFn> + // Wait on __addr while __pred(__vfn()) is false. + // If __bare_wait is false, increment a counter while waiting. + // For callers that keep their own count of waiters, use __bare_wait=true. + template<typename _Tp, typename _Pred, typename _ValFn> void - __atomic_wait_address(const _Tp* __addr, - _Pred&& __pred, _ValFn&& __vfn, + __atomic_wait_address(const _Tp* __addr, _Pred&& __pred, _ValFn&& __vfn, bool __bare_wait = false) noexcept { const auto __wait_addr = @@ -446,6 +441,13 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION _Tp __val = __vfn(); while (!__pred(__val)) { + // If the wait is not proxied, set the value that we're waiting + // to change. + if constexpr (__platform_wait_uses_type<_Tp>) + __args._M_old = __builtin_bit_cast(__detail::__platform_wait_t, + __val); + // Otherwise, it's a proxy wait and the proxy's _M_ver is used. + __detail::__wait_impl(__wait_addr, &__args); __val = __vfn(); } @@ -462,6 +464,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION __detail::__wait_impl(__addr, &__args); } + // Wait on __addr while __vfn() == __old is true. template<typename _Tp, typename _ValFn> void __atomic_wait_address_v(const _Tp* __addr, _Tp __old, diff --git a/libstdc++-v3/include/std/latch b/libstdc++-v3/include/std/latch index de0afd8989bf..c81a6631d53f 100644 --- a/libstdc++-v3/include/std/latch +++ b/libstdc++-v3/include/std/latch @@ -78,8 +78,12 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION _GLIBCXX_ALWAYS_INLINE void wait() const noexcept { - auto const __vfn = [this] { return this->try_wait(); }; - auto const __pred = [this](bool __b) { return __b; }; + auto const __vfn = [this] { + return __atomic_impl::load(&_M_a, memory_order::acquire); + }; + auto const __pred = [](__detail::__platform_wait_t __v) { + return __v == 0; + }; std::__atomic_wait_address(&_M_a, __pred, __vfn); } diff --git a/libstdc++-v3/testsuite/29_atomics/atomic_integral/wait_notify.cc b/libstdc++-v3/testsuite/29_atomics/atomic_integral/wait_notify.cc index c7f8779e4fb2..c6b7f637a2b4 100644 --- a/libstdc++-v3/testsuite/29_atomics/atomic_integral/wait_notify.cc +++ b/libstdc++-v3/testsuite/29_atomics/atomic_integral/wait_notify.cc @@ -33,12 +33,16 @@ template<typename Tp> std::atomic<Tp> a{ Tp(1) }; VERIFY( a.load() == Tp(1) ); a.wait( Tp(0) ); + std::atomic<bool> b{false}; std::thread t([&] { + b.store(true, std::memory_order_relaxed); a.store(Tp(0)); a.notify_one(); }); a.wait(Tp(1)); + // Ensure we actually waited until a.store(0) happened: + VERIFY( b.load(std::memory_order_relaxed) ); t.join(); } -- 2.47.1