C++多线程04:condition_variable(条件变量)

C++多线程:condition_variable

官方定义

在多线程编程中,有一种十分常见的行为:线程同步。线程同步是指线程间需要按照预定的先后次序顺序进行的行为。C++11对这种行为也提供了有力的支持,这就是条件变量(condition_variablecondition_variable_any)。条件变量位于头文件condition_variable下。

condition_variable/condition_variable_any类是一个synchronization primitive,可用于阻止一个线程或同时阻止多个线程,直到另一个线程修改共享变量(condition),并通知condition_variable,才会继续执行。

当调用它的wait函数时,它使用一个mutex来锁定线程。使得该线程保持阻塞状态,直到被另一个线程调用同一个condition_variable对象上的notify函数才被唤醒。condition_variable类型的对象必须使用unique_lock<mutex>等待,而 std::condition_variable_any可以跟任何其他可锁定对象绑定使用, 也可以使用自定义类型。

原理

其实,条件变量跟 cpp11 没特别大关系,它是操作系统实现的(Linux下使用 pthread库中的 pthread_cond_*() 函数提供了与条件变量相关的功能)。现在的关键在于理解为啥要有它,而且需注意一点,条件变量自身并不包含条件。因为它通常和 if (或者while) 一起用,所以叫条件变量。

并发有两大需求,一是互斥,二是等待(同步)。互斥是因为线程间存在共享数据,等待则是因为线程间存在依赖。互斥的话,通过互斥锁能搞定,常见的有依赖操作系统的 mutex。条件变量,是为了解决等待需求。考虑实现生产者消费者队列,生产者和消费者各是一个线程。一个明显的依赖是,消费者线程依赖生产者线程 push 元素进队列。没有条件变量,你会怎么实现消费者呢?让消费者线程一直轮询队列(需要加 mutex)。如果是队列里有值,就去消费;如果为空,要么是继续查,要么sleep一下,让系统过一会再唤醒你,你再次查。可以想到,无论哪种策略,都不通用,要么费cpu,要么线程过分sleep,影响该线程的性能。

有条件变量后,你就可以用事件模式了。上面的消费者线程,发现队列为空,就告诉操作系统,我要wait,一会肯定有其他线程发信号来唤醒我的。这个其他线程,实际上就是生产者线程。生产者线程push队列之后,则调用signal,告诉操作系统,之前有个线程在wait,你现在可以唤醒它了。上述两种等待方式,前者是轮询(poll),后者是事件(event)。一般来说,事件方式比较通用,性能不会太差(但存在切换上下文的开销)。轮询方式的性能,就非常依赖并发pattern,也特别消耗cpu。

条件变量要和锁一起使用,锁提供了互斥这一机制,而条件变量在其基础上提供了同步的机制(同步是比互斥更严格的关系,互斥只要求线程间访问某一资源时不存在同时处理或者交替处理的可能,而对线程本身的调度顺序没有限制,也就是说谁先访问都行但你们一个个来,这就是互斥。同步就是在互斥的基础上,虽然线程之间的调度我们没办法控制,但我们可以原子的让某些线程在唤醒时检查某个条件,如果条件不满足就释放锁然后进入阻塞,通过这种方式达到控制不同线程按照某一种你设定的顺序访问资源)。一般条件变量,锁和用户提供的判定条件这三个因素一起组合使用,上文中的某个条件就是指用户提供的判定条件,而线程在检查这个条件,如果不满足就释放锁然后进入阻塞这个过程的原子性由条件变量提供,这也是条件变量的意义。

condition_variable

今天的目的,我们不介绍系统的条件变量,今天介绍C++11的condition_variable。先来简单介绍一下它的主要成员函数。

wait

// 当前线程的执行会被阻塞,直到收到 notify 为止。
void wait (unique_lock<mutex>& lck);

// 当前线程仅在pred=false时阻塞;如果pred=true时,不阻塞。
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);
复制代码

wait会阻塞当前线程直至条件变量被通知,或虚假唤醒发生。

调用wait时,该函数会自动调用 lck.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。然后阻塞当前执行线程,另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_* 唤醒了当前线程),wait()函数再次调用 lck.lock()重新上锁然后wait返回退出,可以理解lck的状态变换和 wait 函数被调用(退出)是同时进行的。

std::condition_variable提供了两种 wait() 函数。第二种情况多了条件参数 Predicate,只有当 pred 条件为false 时调用 wait() 才会阻塞当前线程,并且在收到其他线程的通知后只有当 predtrue 时才会被解除阻塞。因此第二种情况类似以下代码:

while (!pred()) {
    wait(unique_lock);
}
复制代码

notify

// Unblocks当前正在等待此条件的一个线程。
// 如果没有线程在等待,则函数不执行任何操作。
// 如果有多个线程在等待,它不会指定具体哪个线程。
void notify_one() noexcept;

// Unblocks当前等待此条件的所有线程。
// 如果没有线程在等待,则函数不执行任何操作。
void notify_all() noexcept;
复制代码

notify_one唤醒某个等待(wait)线程。如果当前没有等待线程,则该函数什么也不做,如果同时存在多个等待线程,则唤醒的线程是不确定的(unspecified)notify_all则是唤醒所有在等待的线程。如下例子:

#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

class A {
 public:
  void wait1() {
    std::unique_lock<std::mutex> l(m_);
    cv_.wait(l, [this] { return done_; });
    std::cout << 1 << std::endl;
  }

  void wait2() {
    std::unique_lock<std::mutex> l(m_);
    cv_.wait(l, [this] { return done_; });
    std::cout << 2 << std::endl;
  }

  void signal() {
    {
      std::lock_guard<std::mutex> l(m_);
      done_ = true;
    }
    cv_.notify_all();
  }
 
 private:
  std::mutex m_;
  std::condition_variable cv_;
  bool done_ = false;
};

int main() {
  A a;
  std::thread t2(&A::wait2, &a);
  std::thread t1(&A::wait1, &a);
  std::thread t3(&A::signal, &a);
  t1.join();
  t2.join();
  t3.join();
}  // 12 or 21
复制代码

wait_for

template< class Rep, class Period >
std::cv_status wait_for( std::unique_lock<std::mutex>& lock,
                         const std::chrono::duration<Rep, Period>& rel_time);
                         
template< class Rep, class Period, class Predicate >
bool wait_for( std::unique_lock<std::mutex>& lock,
               const std::chrono::duration<Rep, Period>& rel_time,
               Predicate pred);                   
复制代码

wait_for 导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,或者超时返回。

返回值说明:

  • 若经过 rel_time 所指定的关联时限则为 std::cv_status::timeout ,否则为 std::cv_status::no_timeout 。
  • 若经过 rel_time 时限后谓词 pred 仍求值为 false 则为 false ,否则为 true 。

以上两个类型的wait函数都在会阻塞时,自动释放锁权限,即调用unique_lock的成员函数unlock(),以便其他线程能有机会获得锁。这就是条件变量只能和unique_lock一起使用的原因,否则当前线程一直占有锁,线程被阻塞。

std::condition_variable::wait() 类似,不过 wait_for可以指定一个时间段,在当前线程收到通知或者指定的时间 rel_time 超时之前,该线程都会处于阻塞状态。而一旦超时或者收到了其他线程的通知,wait_for返回,剩下的处理步骤和 wait()类似。

另外,wait_for 的重载版本的最后一个参数pred表示 wait_for的预测条件,只有当 pred条件为false时调用 wait()才会阻塞当前线程,并且在收到其他线程的通知后只有当 predtrue时才会被解除阻塞,因此相当于如下代码:

while (!pred()) {
    if (wait_for(lock, rel_time) == std::cv_status::timeout) {
        return pred();
    }
}
return true;
复制代码

wait_until

template< class Clock, class Duration >
std::cv_status
    wait_until( std::unique_lock<std::mutex>& lock,
                const std::chrono::time_point<Clock, Duration>& timeout_time );

template< class Clock, class Duration, class Pred >
bool wait_until( std::unique_lock<std::mutex>& lock,
                 const std::chrono::time_point<Clock, Duration>& timeout_time,
                 Pred pred );
复制代码

wait_until 导致当前线程阻塞直至通知条件变量、抵达指定时间或虚假唤醒发生,可选的循环直至满足某谓词。

std::condition_variable::wait_for 类似,但是wait_until可以指定一个时间点,在当前线程收到通知或者指定的时间点 abs_time超时之前,该线程都会处于阻塞状态。而一旦超时或者收到了其他线程的通知,wait_until返回,剩下的处理步骤和 wait_until() 类似。

另外,wait_until的重载版本的最后一个参数 pred表示 wait_until 的预测条件,只有当 pred条件为 false时调用 wait()才会阻塞当前线程,并且在收到其他线程的通知后只有当predtrue时才会被解除阻塞,因此相当于如下代码:

while (!pred()) {
    if (wait_for(lock, rel_time) == std::cv_status::timeout) {
        return pred();
    }
}
return true;
复制代码

cv_status

枚举类型,指示函数是否由于超时而返回。该类型是condition_variable和condition_variable_any对象中函数wait_for和wait_until的返回类型。

定义:

enum class cv_status { no_timeout, timeout };
复制代码
  • cv_status::no_timeout :函数在规定时间内返回(例如:被notufy_*唤醒)。
  • cv_status::timeout:函数因超时返回。

wait内部实现

下面用condition_variable_any来举个例子简单说明一下wait函数内部调用的流程

#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>

using namespace std::chrono;

class Mutex {
 public:
  void lock() {
      auto now = steady_clock::now().time_since_epoch();
      std::cout << "lock: " << duration_cast<milliseconds>(now).count() << std::endl;
  }
  void unlock() {
      auto now = steady_clock::now().time_since_epoch();
      std::cout << "unlock: " << duration_cast<milliseconds>(now).count() << std::endl;
  }
};

class A {
 public:
  void signal() {
    std::cout << "notify_one start" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));  // 1
    cv_.notify_one();  // 2
    std::cout << "notify_one end" << std::endl;
  }

  void wait() {
    Mutex m;
    auto now = steady_clock::now().time_since_epoch();
    std::cout << "wait start: " << duration_cast<milliseconds>(now).count() << std::endl;
    cv_.wait(m);  // 3
    auto end = steady_clock::now().time_since_epoch();
    std::cout << "wait end: " << duration_cast<milliseconds>(end).count() << std::endl;
  }

 private:
  std::condition_variable_any cv_;
};
 
int main() {
  A a;
  std::thread t1(&A::signal, &a);
  std::thread t2(&A::wait, &a);
  t1.join();
  t2.join();
}  //
复制代码

语句①,我们先让signal线程等待一段时间(1s)再唤醒wait线程;

语句③调用wait函数会阻塞,大概会阻塞1s钟,但是阻塞前自动释放锁权限,即调用Mutex的成员函数unlock(),当有其他线程notify(语句②)后wait函数返回,重新调用Mutex的成员函数lock()。

输出:

wait start: 21447210   // 1
unlock: 21447210       // 2
notify_one start
notify_one end
lock: 21448210         // 3
wait end: 21448210     // 4
复制代码

从上面的输出也可以看出,语句①和②是几乎同时发生的,也就是调用wait时condition_variable_any会立刻unlock,而当wait返回时则时调用lock,从语句③和④可以得知。

虚假唤醒

我们上面提到很多次虚假唤醒,这里详细解释一下。其实在正常情况下,wait类型函数返回时要不是因为被唤醒,要不是因为超时才返回,但是在实际中发现,因此操作系统的原因,wait类型在不满足条件时,它也会返回,这就导致了虚假唤醒。因此,我们一般都是使用带有谓词参数的wait函数,因为这种(xxx, Predicate pred )类型的函数等价于:

while (!pred()) //while循环,解决了虚假唤醒的问题
{
    wait(lock);
}
复制代码

假设系统不存在虚假唤醒的时,代码只要像下面这样写就可以了:

if (不满足条件) {
    //没有虚假唤醒,wait函数可以一直等待,直到被唤醒或者超时,没有问题。
    //但实际中却存在虚假唤醒,导致假设不成立,wait不会持续等待,会跳出if语句,
    //提前执行其他代码,流程异常
    wait();  
}
复制代码

但因为存在虚假唤醒,所以,正确的方式应该是使用while语句来解决:

while (!(xxx条件) ) {
    //虚假唤醒发生,由于while循环,再次检查条件是否满足,
    //否则继续等待,解决虚假唤醒
    wait();  
}
复制代码

条件变量使用

下面我们使用一个经典的生产者-消费者场景来阐述对于condition_variable的使用,生产者-消费者问题,也称有限缓冲问题,是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”,在实际运行时会发生的问题。

生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。

#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>
#include <queue>
#include <condition_variable>

std::mutex g_cvMutex;
std::condition_variable g_cv;

//缓存区
std::deque<int> g_data_deque;
//缓存区最大数目
const int  MAX_NUM = 30;
//数据
int g_next_index = 0;

//生产者,消费者线程个数
const int PRODUCER_THREAD_NUM  = 3;
const int CONSUMER_THREAD_NUM = 3;

void  producer_thread(int thread_id) {
     while (true) {
         std::this_thread::sleep_for(std::chrono::milliseconds(500));
         //加锁
         std::unique_lock <std::mutex> lk(g_cvMutex);
         //当队列未满时,继续添加数据
         g_cv.wait(lk, [](){ return g_data_deque.size() <= MAX_NUM; });
         g_next_index++;
         g_data_deque.push_back(g_next_index);
         std::cout << "producer_thread: " << thread_id << " producer data: " << g_next_index;
         std::cout << " queue size: " << g_data_deque.size() << std::endl;
         //唤醒其他线程 
         g_cv.notify_all();
     }  //自动释放锁
}

void  consumer_thread(int thread_id) {
    while (true) {
        std::this_thread::sleep_for(std::chrono::milliseconds(550));
        //加锁
        std::unique_lock <std::mutex> lk(g_cvMutex);
        //检测条件是否达成
        g_cv.wait( lk,   []{ return !g_data_deque.empty(); });
        //互斥操作,消息数据
        int data = g_data_deque.front();
        g_data_deque.pop_front();
        std::cout << "\tconsumer_thread: " << thread_id << " consumer data: ";
        std::cout << data << " deque size: " << g_data_deque.size() << std::endl;
        //唤醒其他线程
        g_cv.notify_all();
    }  //自动释放锁
}

int main() {
    std::thread arrRroducerThread[PRODUCER_THREAD_NUM];
    std::thread arrConsumerThread[CONSUMER_THREAD_NUM];

    for (int i = 0; i < PRODUCER_THREAD_NUM; i++) {
        arrRroducerThread[i] = std::thread(producer_thread, i);
    }

    for (int i = 0; i < CONSUMER_THREAD_NUM; i++) {
        arrConsumerThread[i] = std::thread(consumer_thread, i);
    }

    for (int i = 0; i < PRODUCER_THREAD_NUM; i++) {
        arrRroducerThread[i].join();
    }

    for (int i = 0; i < CONSUMER_THREAD_NUM; i++) {
        arrConsumerThread[i].join();
    }
    
    return 0;
}
复制代码

condition_variable_any

std::condition_variable类似,只不过std::condition_variable_anywait 函数可以接受任何 lockable参数,而 std::condition_variable只能接受 std::unique_lock类型的参数,除此以外,和std::condition_variable几乎完全一样。

std::condition_variable_any 能与std::shared_lock一同使用,在std::shared_mutex上以共享所有权模式等待。

condition_variable_any 类是std::condition_variable的泛化。相对于只在std::unique_lock<std::mutex>上工作的std::condition_variablecondition_variable_any 能在任何满足基本可锁定 (BasicLockable) 要求的锁上工作(也可用于自定义可锁定的类型)。

std::condition_variable_any标准布局类型 (StandardLayoutType) 。它非可复制构造 (CopyConstructible)可移动构造 (MoveConstructible)可复制赋值 (CopyAssignable)可移动赋值 (MoveAssignable)

若锁是std::unique_lock,结合std::condition_variable可能会有更好的性能。

notify_all_at_thread_exit

void notify_all_at_thread_exit( std::condition_variable& cond,
                                std::unique_lock<std::mutex> lk );
复制代码

notify_all_at_thread_exit 提供机制,通知其他线程给定的线程已完全完成,包括已经销毁所有thread_local资源。它操作如下:

  • 将先前获得的锁 lk 的所有权转移到内部存储。
  • 修改执行环境,以令当前线程退出时,如同以下列方式通知 condition_variable cond
lk.unlock();
cond.notify_all();
复制代码

当调用该函数的线程退出时,所有在 cond 条件变量上等待的线程都会收到通知。请看下例(参考):

#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id (int id) {
  std::unique_lock<std::mutex> lck(mtx);
  while (!ready) cv.wait(lck);
  // ...
  std::cout << "thread " << id << '\n';
}

void go() {
  std::unique_lock<std::mutex> lck(mtx);
  std::notify_all_at_thread_exit(cv,std::move(lck));
  ready = true;
  std::cout << "ready = true " << '\n';
}


int main ()
{
  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";
 
  std::thread(go).detach();   // go!

  for (auto& th : threads) th.join();

  return 0;
}
复制代码

输出:

10 threads ready to race...
ready = true 
thread 6
thread 7
thread 9
thread 8
thread 5
thread 4
thread 3
thread 2
thread 1
thread 0
复制代码

今天的内容就到这里了。

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
std::condition_variableC++11 标准库中的一个同步原语,用于实现线程间的协作。它通常与 std::unique_lock<std::mutex> 搭配使用,可以实现以下复杂用法: 1. 等待某个条件变量满足特定条件。 ```cpp std::mutex mtx; std::condition_variable cv; bool ready = false; void worker_thread() { // 等待主线程通知 std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return ready; }); // 执行任务 // ... // 通知主线程任务已完成 ready = false; cv.notify_one(); } void main_thread() { // 启动工作线程 std::thread t(worker_thread); // 执行任务 // ... // 通知工作线程任务已完成 { std::lock_guard<std::mutex> lock(mtx); ready = true; } cv.notify_one(); // 等待工作线程完成任务 { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return !ready; }); } // 等待工作线程退出 t.join(); } ``` 2. 等待某个条件变量满足特定条件,并设置超时时间。 ```cpp std::mutex mtx; std::condition_variable cv; bool ready = false; void worker_thread() { // 等待主线程通知 std::unique_lock<std::mutex> lock(mtx); if (cv.wait_for(lock, std::chrono::seconds(1), []{ return ready; })) { // 执行任务 // ... } else { // 超时处理 // ... } // 通知主线程任务已完成 ready = false; cv.notify_one(); } void main_thread() { // 启动工作线程 std::thread t(worker_thread); // 执行任务 // ... // 通知工作线程任务已完成 { std::lock_guard<std::mutex> lock(mtx); ready = true; } cv.notify_one(); // 等待工作线程完成任务 { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return !ready; }); } // 等待工作线程退出 t.join(); } ``` 3. 使用多个条件变量协调多个线程。 ```cpp std::mutex mtx; std::condition_variable cvA, cvB; bool flagA = false, flagB = false; void worker_thread_A() { // 执行任务 // ... // 通知主线程任务已完成 { std::lock_guard<std::mutex> lock(mtx); flagA = true; } cvA.notify_one(); } void worker_thread_B() { // 执行任务 // ... // 通知主线程任务已完成 { std::lock_guard<std::mutex> lock(mtx); flagB = true; } cvB.notify_one(); } void main_thread() { // 启动工作线程 std::thread tA(worker_thread_A), tB(worker_thread_B); // 等待工作线程 A 和 B 完成任务 { std::unique_lock<std::mutex> lock(mtx); cvA.wait(lock, []{ return flagA; }); cvB.wait(lock, []{ return flagB; }); } // 执行任务 // ... // 通知工作线程 A 和 B 任务已完成 { std::lock_guard<std::mutex> lock(mtx); flagA = false; flagB = false; } cvA.notify_one(); cvB.notify_one(); // 等待工作线程 A 和 B 退出 tA.join(); tB.join(); } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁金金

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值