4

C++20 added std::atomic-based thread synchronization methods using wait (std::atomic::wait) and notify (std::atomic::notify_one or std::atomic::notify_all) functions for them. The link https://www.modernescpp.com/index.php/performancecomparison-of-condition-variables-and-atomics-in-c-20 provides examples of using two variables of type std::atomic_flag to transfer control between two threads ("ping-pong game"). Here is the original code for this example (variant "Two Atomic Flags"):

// pingPongAtomicFlags.cpp

#include <iostream>
#include <atomic>
#include <thread>

std::atomic_flag condAtomicFlag1{};
std::atomic_flag condAtomicFlag2{};

std::atomic<int> counter{};
constexpr int countlimit = 1'000'000;

void ping() {
    while(counter <= countlimit) {
        condAtomicFlag1.wait(false);               // (1)
        condAtomicFlag1.clear();                   // (2)

        ++counter;
        
        condAtomicFlag2.test_and_set();           // (4)
        condAtomicFlag2.notify_one();             // (3)
    }
}

void pong() {
    while(counter < countlimit) {
        condAtomicFlag2.wait(false);
        condAtomicFlag2.clear();
        
        condAtomicFlag1.test_and_set();
        condAtomicFlag1.notify_one();
    }
}

int main() {
     auto start = std::chrono::system_clock::now();  

    condAtomicFlag1.test_and_set();                    // (5)
    std::thread t1(ping);
    std::thread t2(pong);

    t1.join();
    t2.join();

    std::chrono::duration<double> dur = std::chrono::system_clock::now() - start;
    std::cout << "Duration: " << dur.count() << " seconds" << std::endl;
}

I changed this code a bit:

#include <iostream>
#include <atomic>
#include <thread>
#include <chrono>

std::atomic_flag condAtomicFlag1{};
std::atomic_flag condAtomicFlag2{};

std::atomic<int> counter{};
constexpr int countlimit = 2;

auto time() {
  static auto start = std::chrono::system_clock::now();
  return std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now() - start).count();
}

void ping() {
    while(counter <= countlimit) {
        condAtomicFlag1.wait(false);               // (1)
        condAtomicFlag1.clear();                   // (2)
        std::cout << time() << " sec: ping() flag1" << std::endl;
        
        ++counter;

        condAtomicFlag2.test_and_set();           // (4)
        condAtomicFlag2.notify_one();             // (3)
    }
}

void pong() {
    while(counter <= countlimit) {
        condAtomicFlag2.wait(false);
        condAtomicFlag2.clear();
        std::cout << time() << " sec: pong() flag2" << std::endl;
        
        condAtomicFlag1.test_and_set();
        condAtomicFlag1.notify_one();
    }
}

int main(int argc, char **argv)
{
  std::cout << time() << " sec: before pong() flag2" << std::endl;
  std::thread t2(pong);
  std::cout << time() << " sec: after pong() flag2" << std::endl;
  std::this_thread::sleep_for(std::chrono::seconds(3));
  condAtomicFlag1.test_and_set();                    // (5)
  std::cout << time() << " sec: flag1 test and set" << std::endl;
  //condAtomicFlag1.notify_one();                      // (6)
  //std::cout << time() << " sec: flag1 notify_one" << std::endl;
  std::this_thread::sleep_for(std::chrono::seconds(3));
  std::cout << time() << " sec: before ping() flag1" << std::endl;
  std::thread t1(ping);
  std::cout << time() << " sec: after ping() flag1" << std::endl;

  t1.join();
  t2.join();

    return 0;
}

Now, at first, only the t2 (pong) thread is started, in which there is a waiting of a change in the state of the atomic flag condAtomicFlag2. Then the main thread goes to sleep for 3 seconds. After that, the state of the atomic flag condAtomicFlag1 (test_and_set) changes, although the thread waiting for it has not been started yet. Then the main thread goes to sleep again for 3 seconds. Finally, thread t1 (ping) is started, in which there is a waiting of a change in the state of the atomic flag condAtomicFlag1. Since earlier condAtomicFlag1 was already changed to true when calling condAtomicFlag1.test_and_set(), the t1 thread immediately starts working and the transfer of control from the t1 thread to the t2 thread and back starts a number of times (countlimit + 1). The application output looks pretty predictable to me:

0 sec: before pong() flag2
0 sec: after pong() flag2
3 sec: flag1 test and set
6 sec: before ping() flag1
6 sec: after ping() flag1
6 sec: ping() flag1
6 sec: pong() flag2
6 sec: ping() flag1
6 sec: pong() flag2
6 sec: ping() flag1
6 sec: pong() flag2

But now I will uncomment two lines in the code of the main function:

condAtomicFlag1.notify_one();                      // (6)
  std::cout << time() << " sec: flag1 notify_one" << std::endl;

I figured this was just a notification to the waiting thread that the state of the condAtomicFlag1 atomic flag has changed. And since we have not yet started a thread that will wait for this particular flag, nothing interesting will happen. But I got this application output:

0 sec: before pong() flag2
0 sec: after pong() flag2
3 sec: flag1 test and set
3 sec: flag1 notify_one
3 sec: pong() flag2
6 sec: before ping() flag1
6 sec: after ping() flag1
6 sec: ping() flag1
6 sec: pong() flag2
6 sec: ping() flag1
6 sec: pong() flag2
6 sec: ping() flag1
6 sec: pong() flag2

Here you can see that on the 3rd second after calling condAtomicFlag1.notify_one(),

3 sec: flag1 notify_one

for some reason, thread t2 exited waiting in the condAtomicFlag2.wait(false) function,

3 sec: pong() flag2

that is, in fact, it waited until the state of the atomic flag condAtomicFlag2 changed. But how could condAtomicFlag1.notify_one() affect the wait for a completely different flag in condAtomicFlag2.wait(false)?

About std::atomic::wait it is written that "These functions are guaranteed to return only if value has changed, even if underlying implementation unblocks spuriously". That is, the return from the function is carried out only when the value changes, even in the case of a false unlock. But I do not see any changes to condAtomicFlag2 in my case, and the return from the function is still carried out?

It looks like I do not understand something in the work of the wait-notify bundle? Explain to me what is happening here and why the notification of one flag affects the state of the other?

P.S. For the project I used the gcc compiler version 11.1.0 (Ubuntu 11.1.0-1ubuntu1 ~ 20.04)

Jarod42
  • 203,559
  • 14
  • 181
  • 302
Elija
  • 91
  • 6
  • Seems fixed with gcc 11.2.0 [Demo](https://godbolt.org/z/bnW9n1fY6) – Jarod42 Sep 30 '21 at 10:01
  • 2
    [Demo](https://godbolt.org/z/qMcq8Ph99) which prints value of `condAtomicFlag2` with `0` for the problematic case. – Jarod42 Sep 30 '21 at 10:06
  • 1
    @Jarod42, you seem to be right. Apparently it was a bug in libstdc++ in gcc-11.1. In gcc-11.2 it has already been fixed. – Elija Sep 30 '21 at 10:07

0 Answers0