这个操作相当于操作系统中的Wait & Signal原语,程序中的线程根据实际情况,将自己阻塞或者唤醒其他阻塞的线程。
个人认为,条件变量的作用在于控制线程的阻塞和唤醒,这需要和锁进行相互配合,用来实现并发程序的控制。
void wait (unique_lock<mutex>& lck);
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);
相当于wait
原语,lck
是传入的锁,如果已经锁定了,那么当前线程(是指拥有lck
锁的那个线程)被阻塞,同时自动调用锁的unlock()
函数,允许其他线程进入临界区;如果使用pred
,那么只有pred
返回false
时,进行阻塞。
void notify_one() noexcept;
唤醒一个被当前条件变量阻塞的线程,如果没有阻塞的线程,那么该函数没有效果。
生产者消费者模型,该模型给出了一个最简单的条件变量与临界区配合的例子:
#include
#include
#include
#include
std::mutex mtx;
std::condition_variable produce, consume;
// 生产者和消费者共享的变量
int cargo = 0;
void consumer() {
std::unique_lock<std::mutex>lck(mtx);
while(cargo == 0) { // 没有货物,消费者阻塞
consume.wait(lck);
}
std::cout << cargo << std::endl; // 表示一次消费
cargo = 0;
produce.notify_one(); // 消费完毕后唤醒生产者
}
void producer(int id) {
std::unique_lock<std::mutex>lck(mtx);
while(cargo != 0) { // 如果有货物,生产者阻塞
produce.wait(lck);
}
cargo = id; // 生产一个货物
consume.notify_one(); // 生产完毕后唤醒一个消费者
}
int main() {
std::thread consumers[10], producers[10];
// 产生生产者和消费者
for(int i = 0; i < 10; ++i) {
consumers[i] = std::thread(consumer);
producers[i] = std::thread(producer, i + 1);
}
// 等待所有线程执行完毕
for(int i = 0; i < 10; ++i) {
producers[i].join();
consumers[i].join();
}
return 0;
}
/*
输出结果:
1
2
3
6
4
7
5
8
9
10
*/
这两个都是条件阻塞(等待)函数。
wait_for
用于控制有时间限制的线程
template <class Rep, class Period>
cv_status wait_for (unique_lock<mutex>& lck,
const chrono::duration<Rep,Period>& rel_time);
template <class Rep, class Period, class Predicate>
bool wait_for (unique_lock<mutex>& lck,
const chrono::duration<Rep,Period>& rel_time, Predicate pred);
在rel_time
内阻塞,如果超过这个时间就自动唤醒,或者是被notify
类的函数唤醒。
代码实例:
#include
#include
#include
#include
#include
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) {
std::cout << "." << std::endl;
}
std::cout << "Yon entered: " << value << std::endl;
th.join();
return 0;
}
wait_until
函数用于等待到指定的时间后自动唤醒或者被notify
类唤醒:
template <class Clock, class Duration>
cv_status wait_until (unique_lock<mutex>& lck,
const chrono::time_point<Clock,Duration>& abs_time);
template <class Clock, class Duration, class Predicate>
bool wait_until (unique_lock<mutex>& lck,
const chrono::time_point<Clock,Duration>& abs_time,
Predicate pred);
同样的,pred
如果是false
,就一直进行wait
。
该函数一次性唤醒所有的阻塞线程,如果没有阻塞线程,则函数没有任何作用。
代码实例:
#include
#include
#include
#include
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 << std::endl;
}
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();
for(auto& th : threads) {
th.join();
}
return 0;
}
/*
输出结果:(顺序会乱)
10 threads ready to race...
thread 9
thread 6
thread 5
thread 2
thread 1
thread 0
thread 8
thread 4
thread 7
thread 3
*/
如果一个线程对临界区加锁,那么只要锁定,其他线程就不能访问该临界区。而条件变量是对锁进行操纵,可以这么理解,每个锁都属于一个线程,对某个锁进行wait
或者notify
大类的操作,相当于对当前拥有这个锁的线程进行操作。
wait
函数阻塞一个线程后,会对锁进行unlock
操作,很显然,如果拥有锁的线程阻塞了,而还不解锁,那么当前的临界区会浪费掉。
每个条件变量可以对多个不同的锁(可以理解为持有锁的不同线程)进行wait或者notify类的操作。上面的生产者消费者模型中,使用两个不同的条件变量,是为了更好的区分。