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

Reply via email to