Is notifiy_one()/notify_all() semantically a release-operation, and if yes, where is that defined?

Viewed 136

Consider the following example from cppreference:

std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
 
void worker_thread()
{
    // Wait until main() sends data
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, []{return ready;});
 
    // after the wait, we own the lock.
    std::cout << "Worker thread is processing data\n";
    data += " after processing";
 
    // Send data back to main()
    processed = true;
    std::cout << "Worker thread signals data processing completed\n";
 
    // Manual unlocking is done before notifying, to avoid waking up
    // the waiting thread only to block again (see notify_one for details)
    lk.unlock();
    cv.notify_one();
}
 
int main()
{
    std::thread worker(worker_thread);
 
    data = "Example data";
    // send data to the worker thread
    {
        std::lock_guard<std::mutex> lk(m);
        ready = true;
        std::cout << "main() signals data ready for processing\n";
    }
    cv.notify_one();
 
    // wait for the worker
    {
        std::unique_lock<std::mutex> lk(m);
        cv.wait(lk, []{return processed;});
    }
    std::cout << "Back in main(), data = " << data << '\n';
 
    worker.join();
}

It is said that notification can and even should be done after releasing the lock. However, I fail to understand how this still provides enough safety regarding the memory order.

Through what mechanism can it be guaranteed that the change of the boolean flag is visible to the other thread by the time that it receives the notification?

I can't find anything that says that notify_one() is in and of itself a release-operation in terms of acquire-release semantics. Thus, it seems to that we cannot be sure that the change in the flag actually happens-before the reception of the notification, and therefore the compiler might even decide to reorder the notification into the preceding mutex-protected block (reordering an instruction from after the unlock to before the lock (!) should not usually "break the contract"), so that the notification is received before the updated flag is observed, and the other thread might wait forever.

In other words: Where in the standard does it say that notify_one "synchronizes-with" receiving a notification?

EDIT: The related question How is std::atomic<T>::notify_all ordered? differs in that it is about the notify_() functions of C++20 atomic and the standard excerpts quoted therein do not apply to condition variables, and as such cannot be considered an answer to my question which is specifically about the standard wording related to CVs and their ordering/timing. The actual answer is likely given by this paragraph of the standard:

http://eel.is/c++draft/thread.condition#4

0 Answers
Related