Skip to content

98 Advanced The Hundred

The Flag the Thread Never Saw

tested at -O0, shipped at -O2

using namespace std::chrono;

bool done{false};

void worker() {
    constexpr long long kGiveUp = 6'000'000'000LL;
    auto t0 = steady_clock::now();
    long long spins = 0;
    while (spins < kGiveUp && !done)   // spin, but never forever
        ++spins;
    std::cout << "worker: " << (spins < kGiveUp ? "saw the flag" : "gave up") << " after "
              << duration_cast<milliseconds>(steady_clock::now() - t0).count() << " ms\n";
}

int main() {
    std::thread w(worker);
    std::this_thread::sleep_for(milliseconds(100));
    std::cout << "main:   setting done = true\n";
    done = true;
    w.join();
}

Build it at -O2 and run. How long before the worker notices?

Answer

Never. It burns all six billion spins and quits, while the same source built at -O0 sees the flag in 100 ms. Typical output (GCC 11.5 on x86-64 Linux; the ms figure wobbles per run):

main:   setting done = true
worker: gave up after 1986 ms

Why

Two threads touch done, one writes, and nothing orders them — a data race, and therefore undefined behavior. The optimizer may then assume no other thread writes done: inside a loop that never writes it the value cannot change, so the read is lifted out of the loop. GCC 11.5 does exactly that from -O1 up, checking the flag once and then spinning on a loop that never touches memory again — at -O2 a single cmp BYTE PTR done[rip], 0 guarding a bare sub rdx, 1 / jne countdown, and at -O3 not even the countdown survives (0 ms, printed before the flag is ever set). At -O0 every read really goes to memory, so the bug passes your debug build and detonates in release. -Wall -Wextra stay silent — a data race is a runtime property, so catching it is -fsanitize=thread's job, not a warning flag's.

The fix

std::atomic<bool> done{false};   // "saw the flag after 100 ms", -O0 through -O3
// volatile only forces the reload: no atomicity, no ordering — not a threading tool in C++

Takeaway: without atomics, the compiler assumes no other thread is touching your variable.

Try it: g++ -std=c++17 -O2 -pthread main.cpp -o demo && ./demo, then -O0. Then change done to std::atomic<bool> and watch every level agree.

Open in Compiler Explorer ↗ Quiz this entry