Search code examples
c++multithreadingcondition-variable

Does it make sense to use different mutexes with the same condition variable?


The documentation of the notify_one() function of condition variable at cppreference.com states the following

The notifying thread does not need to hold the lock on the same mutex as the one held by the waiting thread(s); in fact doing so is a pessimization, since the notified thread would immediately block again, waiting for the notifying thread to release the lock.

The first part of the sentence is strange, if I hold different mutexes in the notifying and notified threads, then the mutexes have no real meaning as the there is no 'blocking' operation here. In fact, if different mutexes are held, then the likelihood that a spurious wake up could cause the notification to be missed is possible! I get the impression that we might as well not lock on the notifying thread in such a case. Can someone clarify this?

Consider the following from cppreference page on condition variables as an example.

std::mutex m;    // this is supposed to be a pessimization
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);   // a different, local std::mutex is supposedly better
    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";
 
    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);   // a different, local std::mutex is supposedly better
        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();
}

PS. I saw a few questions that are similarly titled but they refer to different aspects of the problem.


Solution

  • I think the wording of cppreference is somewhat awkward here. I think they were just trying to differentiate the mutex used in conjunction with the condition variable from other unrelated mutexes.

    It makes no sense to use a condition variable with different mutexes. The mutex is used to make any change to the actual semantic condition (in the example it is just the variable ready) atomic and it must therefore be held whenever the condition is updated or checked. Also it is needed to ensure that a waiting thread that is unblocked can immediately check the condition without again running into race conditions.

    I understand it as follows: It is OK, not to hold the lock on the mutex associated with the condition variable when notify_one is called, or any mutex at all, however it is OK to hold other mutexes for different reasons.

    The pessimisation is not that only one mutex is used, but to hold this mutex for longer than necessary when you know that another thread is supposed to immediately try to acquire the mutex after being notified.

    I think that my interpretation agrees with the explanation given in cppreference on condition variable:

    The thread that intends to modify the shared variable has to

    acquire a std::mutex (typically via std::lock_guard)

    perform the modification while the lock is held

    execute notify_one or notify_all on the std::condition_variable (the lock does not need to be held for notification)

    Even if the shared variable is atomic, it must be modified under the mutex in order to correctly publish the modification to the waiting thread.

    Any thread that intends to wait on std::condition_variable has to acquire a std::unique_lock<std::mutex>, on the same mutex as used to protect the shared variable

    Furthermore the standard expressly forbids using different mutexes for wait, wait_­for, or wait_­until:

    lock.mutex() returns the same value for each of the lock arguments supplied by all concurrently waiting (via wait, wait_­for, or wait_­until) threads.