上一篇文章中《线程互斥》我们讲述了如何使用互斥锁来实现线程间数据的共享和通信,但是互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定(例如:假如如果有锁,但是资源条件不满足,岂不是浪费了锁和cpu资源)。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足
。所以说,条件变量被用来进行线程间的同步。
总结来讲:
同步就是在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免时序问题导致的程序异常,即同步保证了各个执行流对临界资源访问的合理性
。同步实现的例子:
当有资源的时候,可以直接获取资源,没有资源的时候,线程进行等待,等待另外的线程生产一个资源,当生产完成一个资源的时候,再通知等待的线程。
条件变量底层实现原理:
一个PCB等待队列+ 2个接口( 1个是等待接口,一个是唤醒接口)
大概逻辑为下面两种情况:
1.由程序员自己判断没有资源可用的时候,调用等待接口,当前调用等待接口的执行流就被放到了PCB等待队列当中被挂起等待,直到有其他执行流改变(生产)了资源后来唤醒该PCB等待队列当中的执行流。
2.当某一个执行流生产了一个资源之后,调用唤醒接口,当前调用唤醒接口的执行流就会通知PCB等待队列当中被挂起等待的执行流,通知他们可以移出PCB等待队列当中,进而去访问临界资源
注意:条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需程序员自己给出
,例如一个变量是否为0等等,这一点我们从后面的例子中可以看到。线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。这个过程一般用while语句实现
。
条件变量的接口
1.定义条件变量
pthread_ cond _t //条件变量的结构为pthread_cond_t
eg: pthread_cond_t cond;
2.如何初始化条件变量
动态初始化-需要调用销毁接口来进行内存的释放
int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t*attr)
cond:传入条件变量的变量的地址
attr:条件变量的属性, 一般设置为NULL ,采用默认属性
静态初始化-使用宏定义,不需要进行内存释放
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
3.等待条件满足接口
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex)
cond:传入条件变量的变量的地址
mutex:传入互斥锁变量的地址,确保函数操作的原子性
4.唤醒等待接口
int pthread_cond_signal(pthread_cond_t* cond)
通知PCB等待队列当中至少一个执行流
注意:此唤醒接口用来释放被阻塞在条件变量cond上的一个线程。多个线程阻塞在此条件变量上时,哪一个线程被唤醒是由线程的调度策略所决定的。要注意的是,必须用保护条件变量的互斥锁来保护这个函数,否则条件满足信号又可能在测试条件和调用pthread_cond_wait函数之间被发出,从而造成无限制的等待
int pthread_ cond_ broadcast(pthread_cond_t* cond)
通知PCB等待队列当中所有的执行流
这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用这个函数。
5.销毁条件变量
int pthread_cond_destroy(pthread_cond_t* cond)
释放条件变量的内存
使用条件变量和互斥锁模拟简单的生产消费模型
#include
#include
#include
#define THREADCOUNT 2 //每类线程个数
//0代表没有资源可用
//1代表还有资源可用
int g_val = 0;
pthread_mutex_t lock; //定义互斥锁
pthread_cond_t consume_cond; //定义消费者条件变量
pthread_cond_t product_cond; //定义生产者条件变量
void* ConsumeStart(void* arg)//消费者消费逻辑
{
(void)arg;
while(1)
{
pthread_mutex_lock(&lock);//加锁
while(g_val == 0) //循环判断条件
{
//要是没有资源可用,阻塞等待逻辑
//1.先将该PCB放到PCB等待队列当中去
//2.再解掉之前加的锁
//3.等待被唤醒
pthread_cond_wait(&consume_cond, &lock);//消费者的条件变量中等待被 生产资源的生产者改变资源条件来唤醒
}
//有资源可用,则消费
g_val--;
printf("Consumer Consumeend g_val:%d\n", g_val);
pthread_mutex_unlock(&lock);//解锁
//消费完之后,通知唤醒生产者条件变量里的pcb等待队列
pthread_cond_signal(&product_cond);
}
return NULL;
}
void* ProductStart(void* arg)
{
(void)arg;
while(1)
{
pthread_mutex_lock(&lock);//加锁
while(g_val == 1)//判断资源被消费
{
pthread_cond_wait(&product_cond, &lock);//没被消费,暂时不生产,进入等待队列,等待消费者消费资源后唤醒
}
//被消费了
g_val++;//生产
printf("Producer producted g_val: %d\n", g_val);
pthread_mutex_unlock(&lock);//解锁
pthread_cond_signal(&consume_cond);//生产完后通知唤醒消费者条件变量里的pcb等待队列
}
return NULL;
}
int main()
{
//初始化 互斥锁和条件变量
pthread_mutex_init(&lock, NULL);
pthread_cond_init(&consume_cond, NULL);
pthread_cond_init(&product_cond, NULL);
//定义生产者消费者线程
pthread_t Consume_tid[THREADCOUNT];
pthread_t Product_tid[THREADCOUNT];
//pthread_t tid[2];
int i = 0;
int ret = -1;
for(; i < THREADCOUNT; i++)
{
ret = pthread_create(&Consume_tid[i], NULL, ConsumeStart, NULL);//消费者线程
if(ret < 0)
{
perror("pthread_create");
return 0;
}
}
for(i = 0; i < THREADCOUNT; i++)
{
ret = pthread_create(&Product_tid[i], NULL, ProductStart, NULL);//生产者线程
if(ret < 0)
{
perror("pthread_create");
return 0;
}
}
for( i = 0; i < THREADCOUNT; i++)
{
pthread_join(Consume_tid[i], NULL);//等待线程退出回收资源
pthread_join(Product_tid[i], NULL);
}
//销毁互斥锁和条件变量
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&consume_cond);
pthread_cond_destroy(&product_cond);
return 0;
}
要是只有一个pcb等待队列,当消费者可能消费完成之后,通知PCB等待队列的时候,唤醒的同样是一个消费者 ,导致唤醒线程和被唤醒的线程都卡死在pthread_ cond_wait (两个),同理,在这里面生产者线程也永远收不到消费线程的唤醒了,进而,四个线程都卡死在了pthread_cond_wait这个接口当中。
所以要注意:
再来思考等待接口pthread_ cond_wait函数的三个问题:
1为什么需要使用互斥锁?
如果单纯的同步,不保证互斥,也就意味着不同的执行流可以在同一时刻去访问临界资源,所以在条件变量当中需要使用互斥锁来保证互斥属性.保证各个执行流在同一时刻访问临界资源的时候,只有有一个执行流在访问。
条件变量当中的互斥锁是一个道理,从大的角度出发,也是保证了各个执行流在访问临界资源的时候,只有一个执行流可以访问。从条件变量的使用角度而言,还完成了生产线程和消费线程之间的互斥。
总结来讲:互斥用于同步线程对共享数据对象的访问
2 pthread_ cond_wait函数当中的实现逻辑,如何来使用互斥锁?
该接口的内部实现逻辑
总结一下再函数中被唤醒以后的逻辑:
既然已经被唤醒了,说明同步工作已经做完了,接下来就是拿到互斥锁,才可以访问到资源。拿到了就可以返回到用户逻辑执行线程任务,没拿到就会阻塞在函数中等待锁资源。
3.pthread_cond_wait函数实现逻辑当中为什么需要将调用者的PCB先放到PCB等待队列而不是先解锁?
放入等待队列才释放互斥锁是为了防止如果消费者线程先释放互斥锁,被生产者线程抢在消费者线程进入PCB等待队列之前的这个空当期间拿到互斥锁,(可能速度特别快)并且执行生成资源逻辑后都执行唤醒通知PCB等待队列的信号了,而这个时候,再将消费者线程放到PCB等待队列当中(但是已经错过这个信号),就可能造成之后再也没有生产者线程在去调用pthread_ cond_ signal/broadcast这样的接口来唤醒PCB等待队列了。
生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
便于记忆的123规则:
1个场所(队列) +两种角色(消费者和生产者) +三种关系(消费者与消费者互斥+生产者与生产者互斥+消费者和生产者同步加互斥)
生产者与消费者模型的优点
基于BlockingQueue的生产者消费者模型
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)。
使用C++ queue模拟阻塞队列的生产消费模型
#include
#include
#include
#include
#include
#define CAPACITY 10 //队列容量
#define THREADCOUNT 2
class BlockQueue
{
public:
BlockQueue(size_t Capacity) //构造函数实现初始化
{
Capacity_ = Capacity;
pthread_mutex_init(&Mutex_, NULL);
pthread_cond_init(&ConsumeCond_, NULL);
pthread_cond_init(&ProductCond_, NULL);
}
~BlockQueue() //销毁和互斥锁和条件变量
{
pthread_mutex_destroy(&Mutex_);
pthread_cond_destroy(&ConsumeCond_);
pthread_cond_destroy(&ProductCond_);
}
void Push(int& Data) //生产到队列(仓库中)
{
pthread_mutex_lock(&Mutex_);先加互斥锁
while(IsFull()) //判断是否满足该线程任务的前提条件,即仓库满了就阻塞生产
{
pthread_cond_wait(&ProductCond_, &Mutex_);//等待被唤醒,并抢锁资源跳出函数完成线程任务
}
//生产逻辑
Queue_.push(Data);
pthread_mutex_unlock(&Mutex_);//解锁
pthread_cond_signal(&ConsumeCond_);//唤醒消费者等待队列里的消费者
}
void Pop(int* Data) //消费 为出参,接收数据
{
pthread_mutex_lock(&Mutex_);
while(Queue_.empty()) //仓库为空,无法消费,阻塞等待
{
pthread_cond_wait(&ConsumeCond_, &Mutex_);//等待唤醒 并抢锁资源跳出函数完成线程任务
}
*Data = Queue_.front();
Queue_.pop();
pthread_mutex_unlock(&Mutex_);
pthread_cond_signal(&ProductCond_);//唤醒生产队列里的生产者
}
private:
bool IsFull() //判满
{
if(Queue_.size() == Capacity_)
{
return true;
}
return false;
}
private:
std::queue<int> Queue_; //stl里的容器都不是线程安全的,要自己来设计
//定义队列的最大容量
size_t Capacity_;
//互斥
pthread_mutex_t Mutex_;
//同步
pthread_cond_t ConsumeCond_;
pthread_cond_t ProductCond_;
};
void* ConsumeStart(void* arg) //消费逻辑
{
BlockQueue* bq = (BlockQueue*)arg;
while(1)
{
int Data;
bq->Pop(&Data);//Data为出参参数
printf("ConsumeStart [%p][%d]\n", pthread_self(), Data);
}
return NULL;
}
void* ProductStart(void* arg) //生产逻辑
{
BlockQueue* bq = (BlockQueue*)arg;
int i = 0;
while(1)
{
bq->Push(i);
printf("ProductStart [%p][%d]\n", pthread_self(), i);
i++;
}
return NULL;
}
int main()
{
BlockQueue* bq = new BlockQueue(10);
pthread_t com_tid[THREADCOUNT], pro_tid[THREADCOUNT];
int i = 0;
for(; i < THREADCOUNT; i++)
{
int ret = pthread_create(&com_tid[i], NULL, ConsumeStart, (void*)bq);
if(ret < 0)
{
printf("create thread failed\n");
return 0;
}
ret = pthread_create(&pro_tid[i], NULL, ProductStart, (void*)bq);
if(ret < 0)
{
printf("create thread failed\n");
return 0;
}
}
for(i = 0; i < THREADCOUNT; i++)
{
pthread_join(com_tid[i], NULL);
pthread_join(pro_tid[i], NULL);
}
delete bq;
bq = NULL;
return 0;
}