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

posted @ 2023-08-02 22:10  BuzzWeek  阅读(45)  评论(0编辑  收藏  举报