Why does modifying condition for condition variable require owning the mutex?

84 views Asked by At

Consider this case (which is the standard usage in cppreferences):

std::atomic<bool> condition(false);

thread 1:
lock
// check condition the last time before waiting
if (condition == true) { unlock; return; }
// to be clear, I expand "wait" here
{
    // In my understanding
    post task to waiting list
    unlock
    yield
    lock
    check condition
    ...
}

thread 2:
lock
condition=true
notify_one
unlock

I understand that thread 2 needs lock and unlock, so that notify_one won't be called after checking condition but before posting task to waiting list (in my understanding, notify_one does nothing if the task has not been posted to waiting list).

However, I think modifying condition (condition=true in the example) does not require owning the mutex, that is to say, I think this should also work:

thread 2:
condition=true
lock
notify_one
unlock

Because in my understanding, if notify_one is called before thread 1 locks, then the condition check before wait would return true, and thread 1 wouldn't wait at all. If notify_one is called after thread 1 unlocks, the task has already been posted to waiting list, so thread 1 can wake up and check condition, which has already been true.

However, cppreferences says even if the shared variable is atomic, it must be modified while owning the mutex. Is there something I miss?

Related:

Why do pthreads’ condition variable functions require a mutex?

Why does a condition variable need a lock (and therefore also a mutex)

But my question is more specific than them.


Update: I tried to test my idea of modifying condition without owning the mutex, and it works fine so far:

#include <iostream>
#include <atomic>
#include <condition_variable>
#include <mutex>
#include <thread>

std::mutex mu;
std::atomic<bool> condition(false);
std::condition_variable cv;

void thread_1() {
    std::unique_lock<std::mutex> lock(mu);
    if (condition.load() == true)
        return;
    cv.wait(lock);
}
void thread_2() {
    condition.store(true);
    std::unique_lock<std::mutex> lock(mu);
    cv.notify_one();
}

int main() {
    for (size_t i = 0; i < 100000; ++i) {
        std::thread t1(thread_1);
        std::thread t2(thread_2);
        t1.join();
        t2.join();
    }

    return 0;
}
1

There are 1 answers

0
Sven Nilsson On

Typical usage would be as follows:

std::deque<int> shared_data;
bool consumer_waiting = false;
condition_variable cond;
mutex mut;


void produce(int data) {
    bool consumer_was_waiting;
    {
        scoped_lock lock(mut);
        consumer_was_waiting = consumer_waiting;
        if (consumer_was_waiting)
            consumer_waiting = false;
        shared_data.push_back(data);
    }
    if (consumer_was_waiting)
        cond.notify_one(); // this call is outside the mutex scope
}

int consume() {
    scoped_lock lock(mut);
    while (shared_data.empty()) {
        consumer_waiting = true;
        cond.wait(lock);
    }
    return shared_data.pop_front();
}