c++11 std::condition_variable
std::condition_variable
- 需要配合unique_lock使用,
wait(unique_lock<mutex>&)
- notify_one()调用时,只有随机一个wait()线程会得到通知
- notify_all(),所有wait()线程会被通知并得到执行
- wait()调用会阻塞当前线程
// condition_variable example
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
#include <chrono>
using namespace std;
namespace {
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
}
void print_id (int id) {
std::unique_lock<std::mutex> lck(mtx); // 这里必须使用unique_lock
while (!ready) {
cv.wait(lck); // 这里会阻塞
cout << "thead " << id << " notifyed..." << endl; // 被notify后才能执行
}
std::cout << "thread " << id << '\n';
}
void go() {
this_thread::sleep_for(1s); // 观察是否会被抢占mutex而阻塞(不会!)
// std::unique_lock<std::mutex> lck(mtx);
std::lock_guard<std::mutex> lck(mtx); // 这里unique_lock或者lock_gurad都行
ready = true;
this_thread::sleep_for(2s);
// cv.notify_one(); // 只有一个cv.wait(lck)线程会被notify,这里因为所有10个线程都在join,会导致死锁
cv.notify_all(); // 所有cv.wait(lck)线程会被notify
}
void ConditionVariableTest()
{
std::thread threads[10];
// spawn 10 threads:
for (int i=0; i<10; ++i)
threads[i] = std::thread(print_id,i);
std::cout << "10 threads ready to race...\n";
go(); // go!
for (auto& th : threads) th.join();
}
两种等效wait写法
std::unique_lock<std::mutex> lk(mtx);
while(!ready) {
cv.wait(lk);
}
std::unique_lock<std::mutex> lk(mtx);
cv.wait(lk, [](){ return ready;});
唤醒丢失问题
唤醒丢失问题一:在notify之后才执行wait调用
- 在notify之后才执行wait调用
- 可能会导致一直wait阻塞除非再次notify
void print_id2 (int id) {
this_thread::sleep_for(2s); // 当前线程在notify发出之后才被执行
std::unique_lock<std::mutex> lck(mtx); // 这里必须使用unique_lock
while (!ready) { // 避免虚假唤醒
cv.wait(lck); // 这里会解锁并阻塞
cout << "thread " << id << " notifyed... " << endl; // 被notify后才能执行,并重新获得锁
}
std::cout << "thread " << id << '\n';
}
唤醒丢失问题二:wait调用之前被notify线程抢占,导致notify之后才wait
- notify线程没有加锁
- wait线程调用wait()之前被抢占,导致阻塞
先看正常不丢失写法
- notify和wait线程都正确加锁
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <condition_variable>
using namespace std;
mutex mt;
condition_variable cv;
bool ready = false;
void foo() {
lock_guard<mutex> lg(mt);
ready = true;
cout << "foo" << endl;
cv.notify_all();
}
void bar() {
unique_lock<mutex> ulk(mt);
cv.wait(ulk, [](){
return ready;
});
cout << "bar" << endl;
}
int main() {
thread tf(foo); // output: foo
thread tb(bar); // output: bar
tf.join();
tb.join();
return 0;
}
- 模拟唤醒丢失
void foo() {
// lock_guard<mutex> lg(mt); // step 1: 移除这里的锁
this_thread::sleep_for(500ms); // step 2: 让bar先执行进入wait
ready = true; // step 5: foo恢复执行,给ready赋值
cout << __FUNCTION__ << endl;
cv.notify_all(); // step 6: foo发出notify
}
void bar() {
unique_lock<mutex> ulk(mt);
cv.wait(ulk, [](){
bool flag = ready; // step 3: 模拟wait调用执行一半的场景
this_thread::sleep_for(1s); // step 4: 将时间片让给foo
return flag; // step 7: bar获得执行权,继续执行wait(),但已经错过了foo的notify
});
cout << __FUNCTION__ << endl;
}
虚假唤醒问题
虚假唤醒问题一:被唤醒了,但是没有可用资源
- wait中的线程在收到notify_all的信号之后,会都被唤醒并继续执行操作
- 在生产者消费者场景下,临界区资源是有限资源
- 线程被唤醒时,可能资源已经被其它线程用完了,继续执行不满足执行条件
A spurious wakeup happens when a thread wakes up from waiting on a condition variable that's been signaled, only to discover that the condition it was waiting for isn't satisfied.
虚假唤醒问题二:操作系统自发的向wait线程发出notify
To allow for implementation flexibility in dealing with error conditions and races inside the operating system, condition variables may also be allowed to return from a wait even if not signaled
虚假唤醒模拟
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
#include <condition_variable>
using namespace std;
mutex mt;
condition_variable cv;
bool ready {false};
void foo() {
this_thread::sleep_for(500ms); // step 1: 让bar线程先执行并进入wait等待
lock_guard<mutex> lg(mt);
ready = true; // step 3: 获得锁,并生产共享资源
cout << "foo" << endl;
cv.notify_all(); // step 4: 通知所有bar线程
}
void bar(int id) {
unique_lock<mutex> ulk(mt);
cv.wait(ulk, [id](){ // step 2: bar线程获得执行并进入wait,并且会调用一次lambda函数
cout << "thread " << id << " waked up" << endl; // step 5: bar线程收到notify,并执行lambda函数
return ready; // step 8: 其它幸运线程在step5一起被唤醒,但是没有得到资源(虚假唤醒1)
});
cout << "bar: " << id << endl; // step 6: 某个幸运的bar先进入临界区,输出bar
ready = false; // step 7: 幸运线程消耗掉共享资源
}
void spurious_notify() { // 模拟系统的虚假唤醒
while(true) {
this_thread::sleep_for(2s);
cv.notify_all(); // step 9: 再次唤醒所有wait线程,但是他们已经没有可用资源(虚假唤醒2)
}
}
int main() {
thread tf(foo);
thread tbs[3];
thread ts(spurious_notify);
for(int i=0; i<3; i++) tbs[i] = thread(bar, i);
for(auto &t: tbs) t.join();
tf.join();
ts.join();
return 0;
}
/* output
thread 2 waked up
thread 0 waked up
thread 1 waked up // 第一次三个bar线程进入wait,并执行lambda输出
foo // foo线程获得执行,并生产共享资源,发出notify
thread 0 waked up // bar线程0被唤醒
bar: 0 // bar线程0消耗掉了共享资源
thread 1 waked up // bar线程1被唤醒
thread 2 waked up // bar线程2被唤醒
thread 2 waked up // bar线程1再次被唤醒
thread 1 waked up // bar线程2再次被唤醒
thread 2 waked up
thread 1 waked up
thread 2 waked up
thread 1 waked up
thread 1 waked up
thread 2 waked up
thread 2 waked up
thread 1 waked up
thread 1 waked up
thread 2 waked up
*/
wait调用中锁的状态测试
- unique_lock到wait调用之间会进入临界区锁定状态
- wait调用不满足条件判定会继续等待下一次notify,并且释放锁
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
#include <string>
#include <condition_variable>
using namespace std;
mutex mt;
condition_variable cv;
bool ready{false};
void foo(int id) {
while(true) {
this_thread::sleep_for(100ms); // 避免释放资源后立即枪锁
unique_lock<mutex> ul(mt);
cout << "foo " << id << ": win the lock" << endl;
this_thread::sleep_for(100ms); // 观察会不会被抢锁(已lock,不会)
cv.wait(ul, [id](){
this_thread::sleep_for(100ms); // 观察会不会被抢锁(已lock,不会)
cout << "foo " << id << ": begin checking the status" << endl;
this_thread::sleep_for(100ms); // 观察会不会被抢锁(已lock,不会)
cout << "foo " << id << ": checking the status" << endl;
return ready; // 判定结束后没有资源会释放锁
});
cout << "foo " << id << ": consumed the data" << endl;
this_thread::sleep_for(100ms); // 观察会不会被抢锁(已lock,不会)
ready = false; // 消费资源后释放锁
}
}
void bar() {
while(true) {
this_thread::sleep_for(1s); // 让wait先跑
// lock_guard<mutex> lg(mt); // 就不和wait抢锁了
cout << "press any key to notify all: ";
string buff;
getline(cin, buff); // 手动控制notify
ready = true;
cv.notify_all();
}
}
int main() {
thread t1(foo, 1);
thread t2(foo, 2);
thread tb(bar);
t1.join();
t2.join();
tb.join();
return 0;
}
/*
foo 1: win the lock // 第一次foo1先获得锁,wait false之后就释放了锁
foo 1: begin checking the status
foo 1: checking the status
foo 2: win the lock // foo2获得锁,wait false之后就释放了锁
foo 2: begin checking the status
foo 2: checking the status
press any key to notify all:
foo 2: begin checking the status // foo2收到notify,上锁并消费数据,然后释放锁
foo 2: checking the status
foo 2: consumed the data
foo 1: begin checking the status // foo1收到notify,但没有数据可消费,然后释放锁
foo 1: checking the status
foo 2: win the lock // foo2进入新的循环,获得锁,进入wait,然后释放锁
foo 2: begin checking the status
foo 2: checking the status
press any key to notify all:
foo 2: begin checking the status
foo 2: checking the status
foo 2: consumed the data
foo 1: begin checking the status
foo 1: checking the status
foo 2: win the lock
foo 2: begin checking the status
foo 2: checking the status
press any key to notify all:
*/
参考
https://en.m.wikipedia.org/wiki/Spurious_wakeup
https://en.cppreference.com/w/cpp/thread/condition_variable