https://gcc.gnu.org/bugzilla/show_bug.cgi?id=100334
Bug ID: 100334 Summary: atomic<T>::notify_one() sometimes wakes wrong thread Product: gcc Version: 11.0 Status: UNCONFIRMED Severity: normal Priority: P3 Component: libstdc++ Assignee: unassigned at gcc dot gnu.org Reporter: m.cencora at gmail dot com Target Milestone: --- If waiter pool implementation is used in std::atomic<T>::wait/notify for given T, then notify_one must underneath call notify_all to make sure that proper thread is awaken. I.e. if multiple threads call atomic<T>::wait() on different atomic<T> instances, but all of them share same waiter, then notify_one on only one of atomics will possibly wake the wrong thread. This can lead to program hangs, deadlocks, etc. Following test app reproduces the bug: g++-11 -std=c++20 -lpthread #include <atomic> #include <future> #include <iostream> #include <source_location> #include <thread> #include <vector> void verify(bool cond, std::source_location loc = std::source_location::current()) { if (!cond) { std::cout << "Failed at line " << loc.line() << '\n'; std::abort(); } } template <typename T> struct atomics_sharing_same_waiter { std::unique_ptr<std::atomic<T>> a[4]; }; unsigned get_waiter_key(void * ptr) { return std::_Hash_impl::hash(ptr) & 0xf; } template <typename T> atomics_sharing_same_waiter<T> create_atomics() { std::vector<std::unique_ptr<std::atomic<T>>> non_matching_atomics; atomics_sharing_same_waiter<T> atomics; atomics.a[0] = std::make_unique<std::atomic<T>>(0); auto key = get_waiter_key(atomics.a[0].get()); for (auto i = 1u; i < 4u; ++i) { while (true) { auto atom = std::make_unique<std::atomic<T>>(0); if (get_waiter_key(atom.get()) == key) { atomics.a[i] = std::move(atom); break; } else { non_matching_atomics.push_back(std::move(atom)); } } } return atomics; } int main() { // all atomic share the same waiter auto atomics = create_atomics<char>(); auto fut0 = std::async(std::launch::async, [&] { atomics.a[0]->wait(0); }); auto fut1 = std::async(std::launch::async, [&] { atomics.a[1]->wait(0); }); auto fut2 = std::async(std::launch::async, [&] { atomics.a[2]->wait(0); }); auto fut3 = std::async(std::launch::async, [&] { atomics.a[3]->wait(0); }); // make sure the all threads already await std::this_thread::sleep_for(std::chrono::milliseconds{100}); atomics.a[2]->store(1); atomics.a[2]->notify_one(); // changing to notify_all() allows this test to pass verify(std::future_status::timeout == fut0.wait_for(std::chrono::milliseconds{100})); verify(std::future_status::timeout == fut1.wait_for(std::chrono::milliseconds{100})); verify(std::future_status::ready == fut2.wait_for(std::chrono::milliseconds{100})); verify(std::future_status::timeout == fut3.wait_for(std::chrono::milliseconds{100})); atomics.a[0]->store(1); atomics.a[0]->notify_one(); atomics.a[1]->store(1); atomics.a[1]->notify_one(); atomics.a[3]->store(1); atomics.a[3]->notify_one(); }