C++11 条件变量和互斥锁

284 阅读2分钟

条件变量总是与互斥锁配合使用的。这是因为互斥锁可以保证不同的线程对共享变量的互斥访问,而多个线程在同步的时候,我们是需要根据某些条件值来决定是否阻塞当前线程的,并且查看/修改某个变量这个操作必须是互斥的。唤醒/阻塞操作和查看/修改条件值必须是一个原子的,这时就需要加上互斥锁。以一个生产者消费者程序举例:

void produce() {
  vec.push_back(1);
  cv.notify_all();
  std::cout << "producer\n";
}

void consume() {
  //这行代码实际不对,因为必须传入一个锁
  cv.wait();
  std::cout << vec.front() << std::endl;
  vec.pop_back();
  std::cout << "consumer\n";
}

这里操作两个线程同步其实就是vec.empty()这个变量。如果不对上述代码加锁,vector本身不是线程安全的,会出错。 我们将代码加上锁:

void Consume() {
    std::unique_lock<std::mutex> lock(mutex);
    cv.wait(lock);
    std::cout << "consume " << vec.size() << "\n";
}

void Produce() {
    std::unique_lock<std::mutex> lock(mutex);
    vec.push_back(1);
    cv.notify_all();
    std::cout << "produce \n";
}

我们希望的执行顺序是消费者先调用wait阻塞自己,然后生产者生产元素并唤醒消费者。但如果生产者执行cv.notify_all()的时候,消费者还未执行cv.wait(lock)阻塞到该条件变量上,就会丢失该信号,一直得不到唤醒。我们可以通过给代码加上判断语句:如下

void produce() {
  std::unique_lock<std::mutex> lock(mtx);
  vec.push_back(1);
  cv.notify_all();
  std::cout << "producer\n";
}

void consume() {
  std::unique_lock<std::mutex> lock(mtx);
  //加上判断语句
  if(vec.empty()) {
    cv.wait(lock);
  }
  std::cout << vec.front() << std::endl;
  vec.pop_back();
  std::cout << "consumer\n";
}

这样当执行顺序为消费者-生产者的时候,消费者会先阻塞到该条件变量上,然后生产者生产数据并唤醒消费者;如果执行顺序为生产者-消费者,因为此时队列不为空,并不会阻塞自己而直接消费数据。 不过因为操作系统会发生虚假唤醒的情况:即使没有线程调用cv.notify_all唤醒被阻塞的线程,被阻塞的线程依然有可能被唤醒。所以我们需要将if判断改为while。如下:

void produce() {
  std::unique_lock<std::mutex> lock(mtx);
  vec.push_back(1);
  cv.notify_all();
  std::cout << "producer\n";
}

void consume() {
  std::unique_lock<std::mutex> lock(mtx);
  while(vec.empty()) {
    cv.wait(lock);
  }
  std::cout << vec.front() << std::endl;
  vec.pop_back();
  std::cout << "consumer\n";
}