当你不仅想要保护数据,还想对单独的线程进行同步。例如,在第一个线程完成前,可能需要等待另一个线程执行完成。通常情况下,线程会等待一个特定事件的发生,或者等待某一条件达成(为true)。
检查任务完成。要么持续检查mutex,这种方法显然很浪费资源。第二种是每隔一段时间进行一次检查,但是过长过短都不行。
第三种方案是使用条件变量(condition variable),标准库对条件变量提供了两种实现:std::condition_variable和std::condition_variable_any,前者仅限和std::mutex工作,而后者可以与任何满足最低标准的mutex工作(因此加上_any的后缀),更通用也意味着更大的开销,因此一般首选使用前者。
从概念上来说,一个条件变量会与多个事件或其他条件相关,并且一个或多个线程会等待条件的达成。当某些线程被终止时,为了唤醒等待线程(允
许等待线程继续执行)终止的线程将会向等待着的线程广播“条件达成”的信息。
条件变量相关函数
wait(unique_lock &lck)
//Predicate 谓词函数,可以普通函数或者lambda表达式
template< class Predicate >
void wait( std::unique_lockstd::mutex& lock, Predicate pred );
当前线程的执行会被阻塞,直到收到 notify 为止。
notify_one():没有参数、没有返回值。解除阻塞当前正在等待此条件的线程之一。如果没有线程在等待,则还函数不执行任何操作。如果超过一个,不会指定具体哪一线程。
unique_lock和lock_guard都是管理锁的辅助类工具,都是RAII风格;它们是在定义时获得锁,在析构时释放锁。它们的主要区别在于unique_lock锁机制更加灵活,可以再需要的时候进行lock或者unlock调用,不非得是析构或者构造时。
wait函数都在会阻塞时,自动释放锁权限,即调用unique_lock的成员函数unlock(),以便其他线程能有机会获得锁。这就是条件变量只能和unique_lock一起使用的原因,否则当前线程一直占有锁,线程被阻塞。
虚假唤醒问题:由于别的原因导致wait返回(比如notify_all)。所以可以通过while(!pred())循环方式,虚假唤醒发生,由于while循环,再次检查条件是否满足,否则继续等待,解决虚假唤醒。
条件变量一个典型例子就是生产者消费者问题。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。
同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。
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();
//自动释放锁
}
}
所以,同步其实可以看成更严格的互斥。比如生产者消费者的队列是互斥资源,并且还要保证访问顺序,才需要条件变量和锁配合。不满足条件就wait阻塞,释放锁资源给别的线程。满足条件就进行操作。
std::future
C++11提供了std::future类模板,future对象提供访问异步操作结果的机制,很轻松解决从异步任务中返回结果。
唯一期望(unique futures,std::future<>) std::future的实例只能与一个指定事件相关联。
共享期望(shared futures)(std::shared_future<>) std::shared_future的实例就能关联多个事件。
通常,会和std::async一起使用。比如我们要计算一个结果,但是不急着要,就可以用std::async新建一个异步任务执行计算,返回值给future对象。
当你需要这个值时,你只需要调用这个对象的get()成员函数;并且直到“期望”状态为就绪的情况下,线程才会阻塞。
因为std::thread没有提供直接接受返回值的机制。而且如果返回的是互斥资源的指针或引用,会导致互斥保护机制失效
#include <future>
#include <iostream>
int find_the_answer_to_ltuae();
void do_other_stuff();
int main()
{
std::future<int> the_answer=std::async(find_the_answer_to_ltuae);
do_other_stuff();
std::cout<<"The answer is "<<the_answer.get()<<std::endl;
}
std::packaged_task 或 std::promise 也能提供一个std::future对象给该异步操作的创建者
(std::future提供了一个访问异步操作结果的机制,它和线程是一个级别的属于低层次的对象,在它之上高一层的是std::packaged_task和std::promise,他们内部都有future以便访问异步操作结果,std::packaged_task包装的是一个异步操作,而std::promise包装的是一个值,都是为了方便异步操作的。
实际上,用async就可以了)
void wait() const;
当共享状态值是不可以用时,调用wait接口可以一直阻塞,直到共享状态变为"就绪"时,就变为可以用了。
如果我们没有调用wait接口,而是直接调用get接口,它等价于先调用wait()而后在调用get接口,得到异步操作的结果。当调用此方法后 valid() 为 false ,共享状态被释放,即future对象释一次性的事件。