blob: a5c8dd11e6db5f18d04f742ca3e0a116b969a435 [file]
// { dg-do run { target c++20 } }
// { dg-require-gthreads "" }
// { dg-additional-options "-pthread" { target pthread } }
// { dg-add-options libatomic }
// A notify_all() call must not be lost when it follows the store that a
// waiter is waiting for. The library decides whether to issue the
// FUTEX_WAKE by checking a per-address waiter count; that check has to be
// ordered after the caller's store, otherwise the notifier can observe
// "no waiters" (store-buffering reorder) while the waiter observes the old
// value and blocks forever.
//
// This exercises the direct-futex path (a 4-byte scalar on Linux). A
// watchdog thread detects a waiter still blocked after the value already
// changed, recovers with an extra notify_all(), and records the failure.
// The reproduction is timing-dependent (it needs real hardware store
// buffering, seen on x86-64), but on a correct library it always completes
// quickly with no lost wakeups, so the test never produces a false failure.
#include <atomic>
#include <chrono>
#include <thread>
#include <vector>
#include <cstddef>
#include <testsuite_hooks.h>
int
main ()
{
using namespace std::chrono;
constexpr unsigned iterations = 100000;
constexpr unsigned cold_stores = 64; // delay visibility of the store
constexpr unsigned jitter_mod = 4000; // place the notify near the
// waiter's spin -> block window
// Walked by the cold stores below so that each store misses at least the
// L2 cache on common hardware; that is what keeps the store to x pending
// long enough to expose the missing ordering. A fixed size cannot exceed
// every possible last-level cache, so reproduction is best-effort, but the
// test always passes quickly on a correct library.
std::vector<char> buffer (std::size_t (32) << 20);
alignas(64) std::atomic<unsigned> x{0};
alignas(64) std::atomic<unsigned> go{0};
alignas(64) std::atomic<unsigned> done{0};
std::atomic<bool> stop{false};
std::atomic<bool> failed{false};
std::thread waiter([&] {
for (unsigned i = 0; i < iterations; ++i)
{
if (failed.load(std::memory_order_acquire))
break;
go.store(i + 1, std::memory_order_release);
x.wait(i, std::memory_order_acquire);
done.store(i + 1, std::memory_order_release);
}
});
// Keep x's cache line bouncing between cores, which keeps the notifier's
// store to x in its store buffer a little longer and widens the window.
std::thread stirrer([&] {
while (!stop.load(std::memory_order_relaxed))
(void) x.load(std::memory_order_relaxed);
});
// Watchdog: if the waiter has not made progress for a while although x
// already holds the value it is waiting to leave, its wakeup was lost.
std::thread watchdog([&] {
unsigned last = 0;
auto last_progress = steady_clock::now();
while (done.load(std::memory_order_acquire) != iterations
&& !failed.load(std::memory_order_acquire))
{
std::this_thread::sleep_for(milliseconds(5));
const unsigned d = done.load(std::memory_order_acquire);
if (d != last)
{
last = d;
last_progress = steady_clock::now();
}
else if (steady_clock::now() - last_progress > milliseconds(500)
&& x.load(std::memory_order_acquire) == d + 1)
{
failed.store(true, std::memory_order_release);
x.notify_all(); // release the stuck waiter
}
}
});
unsigned seed = 1;
std::size_t pos = 0;
volatile unsigned spin_sink = 0;
for (unsigned i = 0; i < iterations; ++i)
{
while (go.load(std::memory_order_acquire) != i + 1)
if (failed.load(std::memory_order_acquire))
break;
if (failed.load(std::memory_order_acquire))
break;
seed = seed * 1103515245 + 12345;
for (unsigned d = (seed >> 16) % jitter_mod; d != 0; --d)
spin_sink = spin_sink + 1;
for (unsigned k = 0; k < cold_stores; ++k)
{
buffer[pos] = static_cast<char>(i);
pos = (pos + 4160) % buffer.size();
}
x.store(i + 1, std::memory_order_release);
x.notify_all();
while (done.load(std::memory_order_acquire) != i + 1)
if (failed.load(std::memory_order_acquire))
break;
}
stop.store(true, std::memory_order_relaxed);
watchdog.join();
stirrer.join();
// The waiter may be parked in its final wait; wake it so it can exit.
x.store(iterations + 1, std::memory_order_release);
x.notify_all();
waiter.join();
VERIFY( !failed.load() );
return 0;
}