前言
大家好吖,欢迎来到 YY 滴Linux系列 ,热烈欢迎! 本章主要内容面向接触过C++的老铁
主要内容含:
欢迎订阅 YY滴C++专栏!更多干货持续更新!以下是传送门!
目录
- 一.<同步>概念&<条件变量>基本概念
- [二.<条件变量等待> 为什么一定需要 <互斥锁 >?](#二.<条件变量等待> 为什么一定需要 <互斥锁 >?)
- 三.<条件变量>使用规范
- 四.饥饿问题展示------"其他线程改变前,一直处于忙等待"
- 五.条件变量语法
一.<同步>概念&<条件变量>基本概念
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题
- 条件变量: 利用线程间共享的全局变量进行同步的一种机制。它允许一个或多个线程在某个条件满足时进行等待,并在条件满足时被唤醒
- 注意:条件变量本身不是锁,而是与互斥锁(Mutex)结合使用,以确保线程安全
二.<条件变量等待> 为什么一定需要 <互斥锁 >?
- 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
- 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据
三.<条件变量>使用规范
- 等待条件代码
cpp
pthread_mutex_lock(&mutex);
while (条件为假)
{
pthread_cond_wait(cond, mutex);
}
...代码部分
pthread_mutex_unlock(&mutex);
- 给条件发送信号代码
cpp
pthread_mutex_lock(&mutex);
...代码部分改动共享资源,条件变为真
pthread_cond_signal(cond);//给条件发送信号代码
pthread_mutex_unlock(&mutex);
四.饥饿问题展示------"其他线程改变前,一直处于忙等待"
1.基于【普通队列】的<生产者消费者模型>面临的<线程饥饿问题>
- 我们有这样一个场景:
- 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
- 例如:在下面的生产者消费者(普通队列)模型中,一个线程访问队列时,发现队列为空,它只能等待(忙等待),只到其它线程将一个节点添加到队列中
- 这种情况就需要用到条件变量
2.基于【阻塞队列】的<生产者消费者模型>解决<线程饥饿问题>
- 上面提到:生产者消费者(普通队列)模型会面临**<线程饥饿问题>**
- 而阻塞队列则解决了这个问题,用到了**<条件变量>**
- **<条件变量> :**条件变量是利用线程间共享的全局变量进行同步的一种机制。它允许一个或多个线程在某个条件满足时进行等待,并在条件满足时被唤醒。条件变量本身不是锁,而是与互斥锁(Mutex)结合使用,以确保线程安全
- 下面是BlockingQueue的机制:
- 当队列为空时:从队列获取元素的操作将会被 阻塞,直到队列中被放入了元素;
- 当队列满时:往队列里存放元素的操作也会被 阻塞,直到有元素被从队列中取出
- 多线程编程中阻塞队列(Blocking Queue)解决了<线程饥饿问题>
3.<条件变量>实现【阻塞队列】设计部分(图文详细解读)
我们如图所示,在入队列和出队列处分别设置互斥量(锁)和条件变量(锁)
我们拿线程1入队列过程举例:队列满了,在1号条件变量上等待;
队列空了,在2号条件变量上等待
- 线程1生产资源进入队列,上互斥锁,发现不符合1号条件变量的条件(队列没满),解除互斥锁
- 线程1生产资源进入队列,上互斥锁,发现符合1号条件变量(队列满了),在条件变量上等待
- 此时,线程2消费资源出队列,上互斥锁,发现符合2号条件变量(队列非空),解除互斥锁;此时给1号线程条件变量发送信号,唤醒1号条件变量,告诉他" 你不用等了 "
- 此时线程1的条件变量被唤醒,解除互斥锁
4.<条件变量>实现【阻塞队列】代码
- 回顾阻塞队列:
- 注:这里很多条件变量函数我们还没有介绍,我们明白其作用即可,会放到博客最后
- 当队列为空时:从队列获取元素的操作将会被 阻塞,直到队列中被放入了元素;
- 当队列满时:往队列里存放元素的操作也会被 阻塞,直到有元素被从队列中取出
- 代码实现如下:
- 只展示出入队列部分,完整版本在最后
cpp
//_c_cond消费者consumer的条件变量
//_p_cond生产者productor的条件变量
//_q是阻塞队列
void Push(const T &in)//生产者productor
{
pthread_mutex_lock(&_mutex);
if(IsFull())//判断是否满,满了进入阻塞等待
{
//自己,阻塞等待
pthread_cond_wait(&_p_cond,&_mutex);
}
_q.push(in);//生产
//生产了,另一个线程条件变量不符合了,唤醒另一个线程的条件变量阻塞等待
pthread_cond_signal(&_c_cond);
pthread_mutex_unlock(&_mutex);
}
void Pop(T *out)//消费者consumer
{
pthread_mutex_lock(&_mutex);
if(IsEmpty())//判断是否空,空了进入阻塞等待
{
//自己,阻塞等待
pthread_cond_wait(&_c_cond,&_mutex);//伪唤醒状态
}
*out=_q.front();
_q.pop();//消费
//消费了,另一个线程条件变量不符合了,唤醒另一个线程的条件变量阻塞等待
pthread_cond_signal(&_p_cond);
pthread_mutex_unlock(&_mutex);
}
五.条件变量语法
1.条件变量用到的头文件
- 线程库中有互斥锁 和 条件变量
cpp
#include <stdio.h>
#include <pthread.h>
2.条件变量的初始化(动态&静态)
初始化条件变量有两种方法:静态初始化和动态初始化
- 方法1,静态初始化:
- 静态初始化的互斥量不需要显式调用
pthread_cond_destroy
函数进行销毁
cpp
pthread_cond_t cond_d = PTHREAD_COND_INITIALIZER;
- 方法2,动态初始化:
- 动态初始化的条件变量在使用完毕后需要显式调用
pthread_cond_destroy
函数进行销毁
cpp
pthread_cond_t cond;
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attr:条件变量使用的属性对象,通常传递NULL表示使用默认属性。
3.条件变量的销毁
- 动态初始化的条件变量在使用完毕后需要显式调用
pthread_cond_destroy
函数进行销毁
cpp
int pthread_cond_destroy(pthread_cond_t *cond);
返回值:
cpp
int ret = pthread_cond_destroy(&cond_d);
if (ret != 0) {
fprintf(stderr, "Failed to destroy condition variable: %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
4.条件变量的等待
cpp
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量
5.唤醒(一般是其他线程中)条件变量的等待
cpp
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);