1、条件变量
-
在 C++ 11中提供了条件变量(condition_variable)实现多个线程之间的同步操作
-
当条件不满足时,相关线程一直被阻塞,直到某种条件成立,这些线程才会被唤醒。
-
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包含两个动作:
- 一个线程因为等待条件变量的条件成立而挂起,
- 另外一个线程使条件成立,给出信号,从而唤醒被等待的线程。
-
为了防止竞争,条件变量总是和一个互斥锁mutex一起使用。
-
条件变量包含在#include <condition_variable>头文件之中
# 定义一个条件变量
std::condition_variable conditionVariable;
# 返回句柄
conditionVariable.native_handle();
# 通知阻塞在同一把互斥锁mutex上的所有线程
conditionVariable.notify_all();
# 通知阻塞在同一把互斥锁mutex上的一个线程
conditionVariable.notify_one();
# 阻塞在锁上等待notify_one/all的通知
conditionVariable.wait();
# 阻塞在锁上等待notify_one/all的通知,或到达指定的超时时长
conditionVariable.wait_for();
# 阻塞在锁上等待notify_one/all的通知,或到达指定的时间点
conditionVariable.wait_until();
2、wait系列函数
# 阻塞在锁上等待notify_one/all的通知
conditionVariable.wait();
# 阻塞在锁上等待notify_one/all的通知,或到达指定的超时时长
conditionVariable.wait_for();
# 阻塞在锁上等待notify_one/all的通知,或到达指定的时间点
conditionVariable.wait_until();
-
这三个函数的构造函数首先第一个参数都是固定,传入
std::unique_lock<std::mutex>
的类对象 -
另外两个wait_for和wait_until还可以传入时间点或者一个时间片,到达这个时间之后自动释放锁
-
最后三个函数都可以传入一个函数,该函数(lambda表达式)返回值为true或者false。如果不传入也没事,默认为false。
- 如果传入函数或者lambda表达式返回值为true,那么wait*()将会直接返回
- 如果传入函数或者lambda表达式返回值为false,那么wait*()将会解锁互斥量mutex并且阻塞在本行
- 对于阻塞在本行三个函数的效果不同:
- wait将会一直死等、死等到notify_one或者notify_all的通知才会重新尝试加锁
- wait_for会等一个时间片(例如15分钟后)或者收到notify_one或者notify_all的通知才会重新尝试加锁
- wait_until会等一个指定时间点(例如11:00)或者收到notify_one或者notify_all的通知才会重新尝试加锁
-
当收到notify的通知后或者到达时间点或者时间片,wait系列函数开始干活
-
重新加锁,如果重新加锁失败继续释放锁然后继续阻塞
-
加锁成功:
-
如果调用的函数或者lambda表达式返回为false,那么继续释放锁,继续等待
-
如果调用的函数或者lambda表达式返回为true向下执行代码
-
如果没有传入函数或者lambda表达式,那么为true,继续向下执行代码
-
执行完毕临界区代码需要及时的释放掉锁,以防造成死锁
-
-
-
在wait系列函数调用之前首先需要加锁,在对wait函数进行调用,然后再判断条件是否成立,不成立就释放,成立就继续持有!
3、notify系列函数
notify系列拢共2个函数,尝试唤醒线程的数量不同。
-
notify_one:至多通知一个线程,
-
notify_all:至多通知n个线程
-
至多这个词很奇怪,可以换句话来描述:负责叫醒所有持有同一把互斥锁的线程,但有没有用就不知道了。因此它只的工作只是尝试!
-
如果没有线程阻塞在条件变量的锁上,那么通知就是无效的。因此才需要加入至多这个词
# 通知阻塞在同一把互斥锁mutex上的所有线程
conditionVariable.notify_all();
# 通知阻塞在同一把互斥锁mutex上的一个线程
conditionVariable.notify_one();
4、Demo举例子
- 核心内容就是in和out函数中的notify和wait,但是不一定notify是有效的,有可能需要通知的wait线程一个也没有。
- condition_variable的核心其实就是基于Linux系统下的pthread_cond_t的一个变量
#include <iostream>
#include <mutex>
#include <thread>
#include <queue>
#include <condition_variable>
class MessageQueue{
public:
void inMsgRecvQueue(){
for(int i = 0;i < n;i++){
std::unique_lock<std::mutex> uniqueLock(mutex_lock);
std::cout << "inMsgRecvQueue()执行, 插入一个元素i = " << i << std::endl;
q.push(i);
conditionVariable.notify_one();
}
}
void outMsgRecvQueue(){
while(true){
std::unique_lock<std::mutex> uniqueLock(mutex_lock);
conditionVariable.wait(uniqueLock, [this] () {
return q.size() > 0;
});
int msg = q.front();
q.pop();
uniqueLock.unlock();
std::cout << "outMsgRecvQueue()执行, msg = " << msg << std::endl;
}
}
private:
std::queue<int> q;
static const int n = 100000;
std::mutex mutex_lock;
std::condition_variable conditionVariable;
};
void test_mutex_lock_unlock()
{
MessageQueue messageQueue;
std::thread in_thread(&MessageQueue::inMsgRecvQueue, std::ref(messageQueue));
std::thread out_thread(&MessageQueue::outMsgRecvQueue, std::ref(messageQueue));
in_thread.join();
out_thread.join();
}
5、pthread_cond_t
C++11在Linux操作系统下就是基于pthread_cond_t变量进行实现的,加上一些模板与类对齐进行封装
条件变量:
条件变量是需要和信号互斥量mutex一起使用的一个线程同步的控制原语
// 定义一个条件变量, 并且使用静态初始化
1. pthread_cond_t condition = PTHREAD_COND_INITIALIZER;
// 动态初始化条件变量
2. int pthread_cond_init(pthread_cond_t *restrict condition, pthread_condattr_t *restrict attr);
// 阻塞等待
3. int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
// 带时间的阻塞等待
4. int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
/*
最复杂的是wait函数,首先wait函数需要执行很多任务
1). wait函数调用之前首先需要获得信号互斥量的锁,即加锁操作
2). 判断条件是否满足:
1. 条件满足:首先获得了互斥锁,那么访问临界区(公共数据区域)
2. 条件不满足:阻塞,注意这里阻塞在条件变量cond上,放弃锁(解锁)
这也是为什么调用之前需要先加锁
1) 和 2)是一个原子操作,不可分割即无法放弃cpu
3). 当条件满足时阻塞的wait会尝试加锁,然后再访问临界区(公共数据区域)
*/
// 通知至少一个阻塞在条件上的线程
5. int pthread_cond_signal(pthread_cond_t *cond);
// 通知(广播)所有阻塞在条件上的线程
6. int pthread_cond_broadcast(pthread_cond_t *cond);
// 动态初始化的条件变量进行销毁
7. int pthread_cond_destroy(pthread_cond_t *cond);
所有的函数返回值为0表示正常,返回非0表示错误,返回的是错误号。