0

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;
}
5
  • It is the "condition" that needs to be mutex-protected, but the notify_one does not. Check the Boost example, "data_ready": boost.org/doc/libs/1_63_0/doc/html/thread/… Commented Sep 22, 2023 at 16:20
  • @SvenNilsson Sorry to confuse you. I'm aware that modifying condition while owning the mutex but notify_one without owning the mutex also works. I'm just wondering whether it is possible to modify condition without owning the mutex. I have edited the question to make it more clear. Commented Sep 23, 2023 at 2:12
  • No, changing (and reading) "condition" needs to be atomic to avoid a race condition. A common scenario is that consumer uses the condition to determine whether to wait (no data is available), and the producer uses the condition to find out whether the consumer was waiting and then notify_one() needs to be called. Any partially updated data would be catastrophic and may lead to the consumer not waking up when new data is posted. Commented Sep 23, 2023 at 16:51
  • @SvenNilsson In my case changing and reading "condition" are atomic, and the condition is not used to determine whether the consumer is waiting, the producer unconditionally calls notify_one. However, my ultimate goal is indeed avoiding locking and notify_one for producers if the consumer is not waiting, which is only possible if the producers can avoid owning the mutex when modifying the condition. Commented Sep 24, 2023 at 6:19
  • If you always call notify_one, using std::atomic might work but is not particularly efficient because of those unnecessary calls. If your data is something more complex, such as a list or queue that is filled by the producer, that data needs to be mutex protected anyway so atomic will not help much. Commented Sep 24, 2023 at 16:26

1 Answer 1

0

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();
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.