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();
}