This is the link to the core guideline CP.42: Link to core guidelines
Currently I am trying to write a small program using the condition variable in the standard library when I came across this core guidline of using the wait(...)
, it seems that the thread waiting for the condition will be accidentally wake up by some events other than cond.notify_one()
or cond.notify_all()
?
My current approach uses cond.wait(lock)
without the predicate because I am sure there will only be one thread calling the notify_one()
.
My problem is with this code example from the core guideline:
template<typename T>
class Sync_queue {
public:
void put(const T& val);
void put(T&& val);
void get(T& val);
private:
mutex mtx;
condition_variable cond; // this controls access
list<T> q;
};
template<typename T>
void Sync_queue<T>::put(const T& val)
{
lock_guard<mutex> lck(mtx);
q.push_back(val);
cond.notify_one();
}
template<typename T>
void Sync_queue<T>::get(T& val)
{
unique_lock<mutex> lck(mtx);
cond.wait(lck, [this] { return !q.empty(); }); // prevent spurious wakeup
val = q.front();
q.pop_front();
}
And the statement for this example:
Now if the queue is empty when a thread executing get()
wakes up (e.g., because another thread has gotten to get()
before it), it will immediately go back to sleep, waiting.
Well this sounds weird to me (at the line with comment // prevent spurious wakeup
), even if another thread called get()
before the current thread, shouldn't the cond.wait(lck)
block until some other thread called cond.notify_one()
or cond.notify_all()
?
UPDATE
I will show my approach here as a reference of a bad practice to wait without a condition:
#include <iostream>
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <chrono>
class Acquirer;
class Lockable {
// Stores the last acquirer in the queue.
std::atomic<Acquirer *> last_queued = std::atomic<Acquirer *>(nullptr);
friend Acquirer;
};
class Acquirer {
public:
enum Status {IDLE, ACQUIRED, SIGNALED_WAITING};
Acquirer();
void acquire(Lockable *lockable);
void release();
private:
std::mutex blocking_mutex;
std::condition_variable blocking_condition_variable;
Lockable *acquired_lockable;
Acquirer::Status status;
};
Acquirer::Acquirer() {
this->acquired_lockable = nullptr;
this->status = Acquirer::IDLE;
}
void Acquirer::acquire(Lockable *lockable) {
// Put myself in the queue for the lockable, done atomically.
Acquirer *last_queued = lockable->last_queued.exchange(this);
if (last_queued != nullptr) {
// If there are someone in the queue, block myself and wait for the acquirer before me.
{
std::unique_lock<std::mutex> mutex_lock(last_queued->blocking_mutex);
last_queued->blocking_condition_variable.wait(mutex_lock);
}
// Signal the acquirer before me that I am awake.
last_queued->status = Acquirer::SIGNALED_WAITING;
}
// Acquired the lockable.
this->status = Acquirer::ACQUIRED;
this->acquired_lockable = lockable;
}
void Acquirer::release() {
if (this->acquired_lockable == nullptr)
return;
// Attempt to reset the queued acquirer in the lockable.
Acquirer *expected_operator = this;
if (!acquired_lockable->last_queued.compare_exchange_strong(expected_operator, nullptr)) {
// If this operation fails, it is guaranteed someone else have queued after me,
// signal the waiting acquirer thread that it can continue until it replies.
// The subsequent acquirer will set my status to Acquirer::SIGNALED_WAITING if it is awake.
while(this->status != Acquirer::SIGNALED_WAITING) {
this->blocking_condition_variable.notify_one();
}
this->acquired_lockable = nullptr;
}
this->status = Acquirer::IDLE;
}
Lockable lockable;
void execute(Acquirer *acquirer) {
for (int i = 0; i < 10; i++) {
acquirer->acquire(&lockable);
std::cout << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
acquirer->release();
}
}
int main() {
Acquirer acquirer_1;
Acquirer acquirer_2;
Acquirer acquirer_3;
std::thread thread_1(execute, &acquirer_1);
std::thread thread_2(execute, &acquirer_2);
std::thread thread_3(execute, &acquirer_3);
thread_1.join();
thread_2.join();
thread_3.join();
return 0;
}