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)