一、介绍
多线程并发有两大需求:互斥和同步(等待-触发)。互斥是因为线程间存在共享数据,等待-触发是因为线程间存在依赖关系。互斥最常见,一般通过互斥锁unique_lock(mutex)形式实现。同步,由于线程间需要按照预定的先后次序顺序进行,就要用到condition_variable和condition_variable_any。互斥锁条件变量中也要用到,互斥锁是条件变量的应用前提,必须先学会基础的unique_lock(mutex)互斥锁应用。
condition_variable和condition_variable_any基础上,就可以实现事件(event)模式了,事件模式要比轮询模式效率高,在优秀的多线程框架和应用中事件模式是首选。
二、condition_variable
condition_variable是一个能够阻止调用线程直到被通知恢复的对象。condition_variable的成员方法除了构造和析构函数外,区分了两大类:wait(阻塞)类和notify(唤醒)类。本文中wait代表阻塞之意,notify代表唤醒之意,后面加函数或小括号才是指某个具体的函数。wait类函数有wait(),wait_for(),wait_until()三个函数,notify类函数有:notify_one()和notify_all()两个函数。
Wait 阻塞函数
阻塞等待直到被notified唤醒
阻塞等待直到超时唤醒或被notified唤醒
阻塞等待直到截止时间或被notified唤醒
Notify 唤醒函数
随机唤醒一个被wait阻塞的线程
唤醒全部被wait阻塞的线程
当调用线程的一个wait函数时,它使用unique_lock(通过mutex)来锁定线程。该线程保持阻塞状态,直到被另一个调用同一条件变量对象的notify函数的线程唤醒。
condition_variable类型的对象总是使用unique_lock<mutex>来等待:有关其他锁类型的替代方法,请参阅condition_vvariable_any,condition_vvariable_any支持其他类型的锁。
1、wait函数,有两种函数形式
(1)void wait(unique_lock<mutex>&lck); //当前线程的执行会被阻塞,直到收到 notify 为止。
(2)void wait(unique_lock<mutex>&lck,Predicate pred); //当前线程仅在pred=false时阻塞;如果pred=true时,不阻塞。
当前线程调用wait(unique_lock<mutex>&lck)函数时,线程被阻塞,直到收到notify唤醒通知。注意:在调用wait(unique_lock<mutex>&lck)函数时,函数会自动调用lck.unlock()释放mutex,从而允许其他锁定同一mutex的某一线程继续后续执行。这是condition_variable学习的重点,不理解这点,则不能理解condition_variable的使用。
阻塞函数一旦收到唤醒通知(由其他线程明确notify),该函数将取消阻塞并调用lck.lock(),使lck锁定mutex,线程继续独占式执行后续工作。
通常,在另一个线程中调用成员notify_one或成员notify_all来通知阻塞函数唤醒。但某些实现可能会产生虚假的唤醒调用,而不调用这些函数中的任何一个。因此,官方建议使用该功能的用户应确保满足其恢复条件。虚假唤醒的情形,作者目前还没有遇到。
2、wait_for函数,有两种函数形式
wait_for (unique_lock<mutex>&lck, const chrono::duration<Rep,Period>& rel_time);
wait_for (unique_lock<mutex>&lck,const chrono::duration<Rep,Period>& rel_time, Predicate pred);
当前线程调用wait_for(lck,rel_time)函数在rel_time期间被阻止,或者直到被notify唤醒(如果notify先发生)。在阻塞线程时,wait_for(lck,rel_time)函数会自动调用lck.unlock(),允许其他锁定的线程继续。
阻塞线程一旦收到唤醒通知(由其他线程明确notify)或者超时唤醒,wait_for(lck,rel_time)函数将取消阻塞并调用lck.lock(),使lck锁定mutex,线程继续独占式执行后续工作。
Wait_for(lck,rel_time,pred)在wait_for(lck,rel_time)基础上增加了pred参数,当前线程仅在pred=false时阻塞;如果pred=true时,不阻塞。
3、wait_until函数,有两种函数形式
wait_until(unique_lock<mutex>&lck,const chrono::time_point<Clock,Duration>& abs_time);
wait_until (unique_lock<mutex>&lck,const chrono::time_point<Clock,Duration>& abs_time,Predicatepred);
与wait_for 类似,wait_until(lck,abs_time)可以指定一个截止时间点,在当前线程收到notify通知或者到达指定的截止时间点 abs_time超时之前,该线程都会处于阻塞状态。
阻塞线程一旦收到唤醒通知(由其他线程明确notify)或者超时唤醒,wait_until (lck, abs_time)函数将取消阻塞并调用lck.lock(),使lck锁定mutex,线程继续独占式执行后续工作。
Wait_until(lck,abs_time,pred)在wait_until(lck,abs_time)基础上增加了pred参数,当前线程仅在pred=false时阻塞;如果pred=true时,不阻塞。
4、notify_one
void notify_one() noexcept;
notify_one()唤醒相同condition_variable阻塞的一个线程。如果有多个线程,则未指定选择哪个线程,唤醒的线程是随机的。如果没有线程在等待,则函数不执行任何操作。
5、notify_all
void notify_all() noexcept;
notify_all()唤醒相同condition_variable阻塞的所有线程。如果没有线程在等待,则函数不执行任何操作。
三、condition_variable应用示例
1、condition_variable::wait(with predicate)示例
//condition_variable::wait (with predicate)
#include <iostream> // std::cout
#include <thread> // std::thread, std::this_thread::yield
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx;
std::condition_variable cv;
int cargo = 0;
bool shipment_available() {return cargo!=0;}
void consume (int n) {
for (int i=0; i<n; ++i) {
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck,shipment_available);//shipment_available为 false时阻塞;为true时,不阻塞。
// consume:
std::cout << cargo << '\n';
cargo=0;
}
}
int main ()
{
std::thread consumer_thread (consume,10);
// produce 10 items when needed:
for (int i=0; i<10; ++i) {
while (shipment_available()) std::this_thread::yield();//为true时,则将本时间片让渡给其他线程,主线程继续执行while循环。为false时,退出循环,执行下面代码。
std::unique_lock<std::mutex> lck(mtx);
cargo = i+1;
cv.notify_one();
}
consumer_thread.join();
return 0;
}
看懂这段代码的前提条件:n-2次提醒
当前线程调用wait(unique_lock<mutex>&lck)函数时,线程被阻塞,直到收到notify唤醒通知。注意:在调用wait(unique_lock<mutex>&lck)函数时,函数会自动调用lck.unlock()释放mutex,从而允许其他锁定同一mutex的某一线程继续后续执行。这是condition_variable学习的重点,不理解这点,则不能理解condition_variable的使用。
阻塞函数一旦收到唤醒通知(由其他线程明确notify),该函数将取消阻塞并调用lck.lock(),使lck锁定mutex,线程继续独占式执行后续工作。
程序输出:
1 2 3 4 5 6 7 8 9 10 |
2、condition_variable::wait_for示例
//condition_variable::wait_for example
#include <iostream> // std::cout
#include <thread> // std::thread
#include <chrono> // std::chrono::seconds
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable, std::cv_status
std::condition_variable cv;
int value;
void read_value() {
std::cin >> value;
cv.notify_one();
}
int main ()
{
std::cout << "Please, enter an integer (I'll be printing dots): \n";
std::thread th (read_value);
std::mutex mtx;
std::unique_lock<std::mutex> lck(mtx);
while (cv.wait_for(lck,std::chrono::seconds(1))==std::cv_status::timeout) {//每超时一秒则打印一个'.',并且超时后会继续循环,直到被notify唤醒才退出循环
std::cout << '.' << std::endl;
}
std::cout << "You entered: " << value << '\n';
th.join();
return 0;
}
看懂这段代码的前提条件:n-1次提醒
当前线程调用wait_for(lck,rel_time)函数在rel_time期间被阻止,或者直到被notify唤醒(如果notify先发生)。在阻塞线程时,wait_for(lck,rel_time)函数会自动调用lck.unlock(),允许其他锁定的线程继续。
阻塞线程一旦收到唤醒通知(由其他线程明确notify)或者超时唤醒,wait_for(lck,rel_time)函数将取消阻塞并调用lck.lock(),使lck锁定mutex,线程继续独占式执行后续工作。
程序可能的输出(程序运行后,等待输入时间越长,打印的'.'越多):
Please, enter an integer (I'll be priniting dots): . . 7 You entered: 7 |
3、condition_variable::wait(withoutpredicate)示例
//condition_variable example
#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);
ready = true;
cv.notify_all();
}
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";
go(); // go!
for (auto& th : threads) th.join();
return 0;
}
看懂这段代码的前提条件:n次提醒
当前线程调用wait(unique_lock<mutex>&lck)函数时,线程被阻塞,直到收到notify唤醒通知。注意:在调用wait(unique_lock<mutex>&lck)函数时,函数会自动调用lck.unlock()释放mutex,从而允许其他锁定同一mutex的某一线程继续后续执行。这是condition_variable学习的重点,不理解这点,则不能理解condition_variable的使用。
阻塞函数一旦收到唤醒通知(由其他线程明确notify),该函数将取消阻塞并调用lck.lock(),使lck锁定mutex,线程继续独占式执行后续工作。
可能的输出(线程执行顺序是变化的):
10 threads ready to race... thread 2 thread 0 thread 9 thread 4 thread 6 thread 8 thread 7 thread 5 thread 3 thread 1 |
下一节介绍condition_variable_any,请参考《C++ 多线程同步condition_variable_any的用法》。