线程安全--锁+条件变量-queue模拟阻塞队列的生产者与消费者模型,信号量-环形队列的生产者与消费者模型

**

生产者与消费者生产模型

**
这个是一种典型的设计模式–大佬们针对典型的应用场景设计的解决方案
在这里插入图片描述
为什么使用生产者与消费者模型?
解决大量的数据产出以及处理的问题。
通过一个容器解决生产者与消费者的强耦合问题(程序中的函数也是为了解决功能的强耦合问题),生产者与消费者使用一个阻塞队列来进行通讯,生产者产出数据直接扔给阻塞队列,消费者直接从阻塞队列中取,阻塞队列相当于缓冲区,平衡生产者消费者之间的处理能力。有线程不断生产数据,有线程不断处理数据。。
总结原则
1个场所
两个角色(生产者,消费者)
三种关系(生产者与生产者的互斥关系,消费者与消费者的互斥关系,生产正者与消费者的互斥与同步关系)
使用阻塞队列来处理生产者消费者的什么问题,以及有什么好处
1、解耦合
如果生产者与消费者在用一个执行流,那么只能生产一个解决一个,依赖性太强,功能耦合高
2、支持忙 闲 不均
在一边压力大一边压力小的时候,缓冲区队列缓冲大量数据,慢慢处理。
3、支持并发
支持多个消费者多个生产者,前提中间的缓冲区操作必须线程安全。

额外知识–并发并行理解
并发–轮询处理–多个执行流在cpu的调度下进行轮询处理。
并行–同时处理—在cpu条件足够的情况下,同时处理。
并行这个概念一般少说,除非cpu的资源非常多,可以支持并行。
有多线程的并发—操作系统层面的轮询调度(或者cpu资源足够多的情况下的并行)
后边网络编程讲到的多路转接模型–高并发服务器模型—应用层面的任务轮询处理

阻塞队列是一种经常实现生产者和消费者模型的数据结构。
针对于不同的线程来讲,与普通队列在于,
队列为空时,从队列拿元素会被阻塞,直到队列中放入了元素;
队列满了时,往队列放元素会被阻塞,直到队列中取出了元素。
在这里插入图片描述
形成自己的代码风格
输入型参数:一个数据传递给函数,函数内部使用就可以。const &
输出型参数:通过参数从函数内部获取数据的结果。从函数内部向外传输数据 int*
输入输出型参数:一个数据传递给函数使用并且传出数据处理的结果。int&

生产者消费者的实现–一个场所,两种角色,三种关系
对于生产者、消费者
其实只是两种业务处理的线程而已–我们创建线程就可以。
对于线程安全的队列–关键
装一个线程安全的BlockQueue–阻塞队列–向外提供线程安全的入队/出队操作

//阻塞队列的实现
#include<cstdio>                                   
#include<iostream>
#include<queue>
#include<pthread.h>
#define QUEUE_MAX 5//queue的大小,用宏比较好
class BlockQueue
{
   private:
      //queue在BlockQueue类中,所以这个queue的类型是BlockQueue
      //Blockqueue *queue;queue可以调用这个类中的所有接口pushpop等
      std::queue<int> _queue;//STL的容器不是线程安全的,STL设计是奔着性能去的,这里要保证安全,所以要使用mutex.
      int _capacity;//数据不能无限添加,内存耗尽程序就崩溃
      pthread_mutex_t  mutex; //互斥锁,为了保证容器安全
      pthread_cond_t _productor_cond;//生产者队列,
      pthread_cond_t _customer_cond;//消费者队列,大家各自拥有自己的队列,在唤醒的时候不容易冲突。
   public:
      //queue是std里边的不需要初始化,
      //容量capacity肯定要初始化,capacity是一个栈上的变量,所以不用destory
      //mutex,cond在学习时候就是必须要初始化的,并且用完还要进行destory.
     BlockQueue(int max = QUEUE_MAX);//构造函数,参数给了一个缺省值              
                       :capacity(max)
                       这个由于queue调用了push函数,在push的时候开空间,但是下边的vector是直接使用vector[_step_read]等等,所以要提前给其开空间
      {
          pthread_mutex_init(&_mutex,NULL);//在构造函数中尽量写能成功初始化的函数
          pthread_cond_init(&_pro_cond,NULL);
          pthread_cond_init(&_cus_cond,NULL);
      }
      
      ~BlockQueue();//构造函数
      {
      	  pthread_mutex_destroy(&_mutex);
          pthread_cond_destroy(&_pro_cond);
          pthread_cond_destroy(&_cus_cond);
      }
      /入队是要访问queue的,它属于临界资源,是需要被保护的。
      bool Push(const int& data);//入队,生产者将数据放在缓冲区队列中
      {
      	  pthread_mutex_lock(&_mutex);
      	  while(_queue.size() == _capacity)//访问临界资源,首先加锁
      	  {
      	  		//满了,则等待
      	  		pthread_cond_wait(&_pro_cond,&_mutex);//解锁 等待 被唤醒加锁一步完成
      	  }
      	  //1、被唤醒后加锁然后发现queue没满,2、或者第一次下来就发现queue没满
      	   _queue.push(data);
      	    pthread_mutex_unlock(&_mutex);//入完队就发现queue不访问了,解锁
      	     pthread_cond_signal(&_cus_cond);
             return true;//返回表示入队成功了
      }
      bool Pop(int *data);//出队,消费者拿到缓冲区的数据 
      {
          pthread_mutex_lock(&_mutex);
           while(_queue.empty())
      {
          pthread_cond_wait(&_cus_cond,&_mutex);
      }
       *data = _queue.front();//获取队首结点的数据
        _queue.pop();
        pthread_mutex_unlock(&_mutex);
        pthread_cond_signal(&_pro_cond);
        return true;
      }
 //生产者的线程的操作
  void *thr_productor(void* arg)
  {
  		//线程需要操作queue,给队列进行入队,所以需要将queue当作线程入口函数的参数传递进来。
  		//但是在互斥锁实现以及厨师与顾客的实现中,线程的参数都是NULL,那是因为bowl和ticket都是全局变量。
  		//入口函数的参数类型是(void*),需要强转为BlockQueue*
  		Blockqueue *queue = (Blockqueue*)arg;
  		int i = 0;//数据
  		while(1)//因为创建了入口函数,进来了之后,需要不停的让线程push数据,但是线程只创建一次,push数据要不停,所以在线程函数这要用while(1)
  		{
  			 queue->push(i);
             printf("productor push data:%d\n",i++);
  		}
  		return NULL;//当线程被外界中断的时候,线程退出,反回
  }
  void *thr_customer(void* arg)
  {
  		Blockqueue* queue = (Blockqueue*)arg;
  		while(1)
  		{
  		 	 int data;//这块一般不容易想到,消费者要定义一个变量data去拿数据
             queue->pop(&data);//Pop(int *data)这个data是拿数据
         //在函数里边直接是*data = 队列头数据,就把数据拿出来了
             printf("customer pop data:%d\n",data);
  		}
  		 return NULL;
  }
   int main()
   {
    	 int ret,i;
    	  pthread_t ptid[4],ctid[4];//创建4个线程生产数据,消费数据
    	  因为在thr_productor和thr_customer函数中需要访问同一个队列,所以队列要定义在主函数中,
    	   //因为queue这个临界资源是不安全的,需要mutex和cond,我们需要将其三者构造成一个类,类的实例化只能是局部变量,所以要把queue在创建线程时当作参数传递过去
    	  //在创建线程的时候,通过(void*)&queue参数形式传递到函数,并且经过强转得到对象的地址,然后在线程函数中访问类队列的成员函数
    	  Blockqueue queue(20);//创建一个类的对象,也是类型//20是队列的大小
    	  //创建生产者线程
     for(i = 0; i < 4; i++)
     {
        ret= pthread_create(&ptid[i],NULL,thr_productor,(void*)&queue);//注意这里传的是queue的地址,所以要&
        if(ret != 0)
        {
          printf("creat productor thread error\n");
          return -1;
        }	  
     }
         //创建消费者线程
      for(i = 0; i < 4; i++)
     {
        ret= pthread_create(&ptid[i],NULL,thr_customer,(void*)&queue);//注意这里传的是queue的地址,所以要&
        if(ret != 0)
        {
          printf("creat customer thread error\n");
          return -1;
        }	  
     }
      for(i = 0; i < 4; i++)
      {
         pthread_join(ptid[i],NULL);//线程要等待,回收资源,或者在入口函数那块detach线程。
         pthread_join(ctid[i],NULL);
      }
       return 0;
116 }                                    

**
这个程序可能会出现只有五个队列,但是会一次性get6个数据的情况,这是因为push和printf不是原子操作。

信号量POSIX标准

**
互斥锁的本质
一个0/1的计数器,实现互斥
条件变量cond的本质是
两个接口(wait,post/broadcast)+pcb等待队列,实现同步。
但是在厨师与顾客,生产者与消费者的模型中由于有bowl和queue的公共资源,所以还得使用mutex来实现。

信号量本质:一个计数器(可以记录很多资源)+pcb等待队列
信号量功能:实现进程/线程的同步与互斥(主要实现同步)

信号量同步的实现:–sem_t idle//sem_t data
通过计数器对资源进行计数,在 生产/获取资源 之前可以先通过计数得知访问的合理性。进行操作
p操作:访问资源之前访问信号量计数,访问合理,资源-1,不合理阻塞
v操作:生产资源之前访问信号量计数,访问合理,资源+1,唤醒一个阻塞的线程或者进程,不合理返回。
信号量互斥实现–sem_t lock:
只有一个计数,表示只有一个资源,同一时间只有一个线程或者进程可以访问。
0、定义信号量
sem_t sem;
1、初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
pshared:0表示线程间共享,1表示进程间共享。
value:信号量初始值,也是资源的计数。
2、访问临界资源之前进行p操作,让资源数-1—等待信号量
int sem_wait(sem_t *sem);通过自身计数判断是否满足访问条件,不满足则一直阻塞线程/进程
int sem_trywait(sem_t *sem);通过自身计数判断是否满足访问条件,不满足立即报错返回 ETIMEDOUT
int sem_timewait(sem_t *sem,timespec *timeout);不满足则等待指定时间,超时后报错返回-ETIMEDOUT
wait(互斥锁信号量),资源-1。相当于加锁
wait(空闲空间信号量)//可以访问,资源-1,不可以访问阻塞
wait(数据信号量)//可以访问,资源-1,不可以访问阻塞
3、生产资源之后进行v操作,让资源数+1,唤醒阻塞线程–发布信号量
int sem_post(sem_t *sem);
post(互斥锁信号量),资源+1。相当于解锁
post(空闲空间信号量)//可以访问,资源+1,不可以访问阻塞
post(数据信号量)//可以访问,资源+1,不可以访问阻塞

wait是一个让信号量-1的操作,访问哪个信号量,就让哪个信号量-1
post是一个让信号量+1的操作,访问哪个信号量,就让哪个信号量+1

4、销毁信号量
int sem_destroy(sem_t *sem);

上一个生产者消费者的例子是基于queue的,其空间可以动态分配,因为queue可以动态增长,
现在基于固定大小的环形队列重写这个程序(POSIX信号量):

基于环形队列的生产者消费者模型
在这里插入图片描述
在这里插入图片描述

class Ringqueue
{
//因为这个是资源一个一个+/-。需要迭代器指针来回跑,所以使用vector,也可以用list等等
  private:
	 std::vector<int> _queue;//数组
	 int _capacity;//这是队列的容量
	 int _step_read;//获取数据位置下标
     int _step_write;//写入数据位置下标
     sem_t  _lock;//这个信号量用于实现互斥,计数器=1+等待队列
     sem_t  _sem_idle;//这个信号量用于计数空闲空间,生产者访问这个信号量,初始值为结点的个数。
     sem_t  _sem_data;//这个信号量用于计数数据空间,消费者访问这个信号量。初始值为0.
   public:
       Ringqueue(int maxq = QUEUE_MAX)
                :_capacity(maxq)
                ,_vector(maxq)//上边queue初始化没有这个
 由于上边的queue调用了push函数,在push的时候开空间,但是这个vector是直接使用vector[_step_read]等等,所以要提前给其开空间
                ,_step_read(0)//这两个一定要进行初始化,否则会是随机值,导致运行的时候程序崩溃
                ,_step_write(0)//这两个一定要进行初始化,否则会是随机值,导致运行的时候程序崩溃,
                //并且不管是放完数据,还是取完数据,_step_read,_step_write都是++,然后在空间上移动,只有资源数是在wait访问下-1,post访问下+1.
       {
          sem_init(&_lock,0,1);//互斥锁只需要一个资源
          sem_init(&_sem_data,0,0);//数据刚开始的资源是0
          sem_init(&_sem_idle,0,maxq);//空闲空闲的计数是max最大值
          ///上边的sem_data,sem_idle在操作的过程中,他们的第三个参数的技术值进行+/-
       }
        ~Ringqueue()
        {
           sem_destroy(&_lock);
           sem_destroy(&_sem_data);
           sem_destroy(&_sem_idle);
        }
		 bool push(int data)//入队数据应该是
		 {
		    //入队应该使用wait()先访问空闲的信号量
		    //可以访问,计数-1,继续向下走
		    //不能访问阻塞
		    sem_wait(&_sem_idle);
		    //只要走下来就是可以访问,访问临界资源要进行加锁
		    sem_wait(&_sem_lock);//加锁
		    //访问资源
		     _vector[_step_write] = data;
		     _step_write = (_step_write + 1)% _capacity;//走到最后,从头开始
		     //访问完资源,解锁
		     sem_post(&_lock);//唤醒加锁信号量,资源+1,相当于解锁
		     sem_post(&_sem_data); //给空间加了数据,就要去唤醒数据信号量,资源+1
		     return true;
		 }
		 bool pop(int *data)//出数据
		 {
		    //先访问一下有数据信号量
		    sem_wait(&sem_data);
		    //可以访问,data计数-1,然后进行访问资源,先进行加锁
		    //不可以访问则阻塞
		    sem_wait(&sem_lock);
		    *data = _vector[_read_step];
		     _step_read = (_step_read + 1) % _capacity;
		     sem_post(&_lock);//唤醒加锁信号量,资源+1,相当于解锁
		     sem_post(&_sem_idle); //给空间减了数据,就要去唤醒空闲信号量,资源+1
		     return true;
		 }
         void* thr_productor(void* arg)  
         {
         	Ringqueue* vector = (Ringqueue*)arg;
         	int i = 0;//生产者生产的数据
         	while(1)
            {
               vector->push(i);
               printf("productor push data:%d\n",i++);
            }
            return NULL;
         }
         void* thr_customer(void* arg)
         {
             Ringqueue* vector = (Ringqueue*)arg;
             while(1)
             {
               int data;//这块消费者需要定义一个data变量去拿数据
               vector->pop(&data);//这里边data的数据已经传地址拿到了,可以不用data接收,就这样就可以。
               printf("customer pop data:%d\n",data);
             }
             return NULL;
         }
   int main()
   {
    	 int ret,i;
    	  pthread_t ptid[4],ctid[4];//创建4个线程生产数据,消费数据
    	  因为在thr_productor和thr_customer函数中需要访问同一个队列,所以队列要定义在主函数中,
    	   //因为vector这个临界资源是不安全的,需要mutex和cond,我们需要将其三者构造成一个类,类的实例化只能是局部变量,所以要把queue在创建线程时当作参数传递过去
    	  //在创建线程的时候,通过(void*)&queue参数形式传递到函数,并且经过强转得到对象的地址,然后在线程函数中访问类队列的成员函数
    	  Blockqueue vector(20);//创建一个类的对象,也是类型//20是队列的大小
    	  //创建生产者线程
     for(i = 0; i < 4; i++)
     {
        ret= pthread_create(&ptid[i],NULL,thr_productor,(void*)&vector);//注意这里传的是queue的地址,所以要&
        if(ret != 0)
        {
          printf("creat productor thread error\n");
          return -1;
        }	  
     }
         //创建消费者线程
      for(i = 0; i < 4; i++)
     {
        ret= pthread_create(&ptid[i],NULL,thr_customer,(void*)&vector);//注意这里传的是queue的地址,所以要&
        if(ret != 0)
        {
          printf("creat customer thread error\n");
          return -1;
        }	  
     }
      for(i = 0; i < 4; i++)
      {
         pthread_join(ptid[i],NULL);//线程要等待,回收资源,或者在入口函数那块detach线程。
         pthread_join(ctid[i],NULL);
      }
       return 0;
116 }        
}

对于消费者生产者模型的阻塞任务队列类总结class BlockQueue{}
1、首先我们先定义一个阻塞的任务队列,第一个用queue是实现,第二个是一个环形的,使用vector来实现
2、由于queue和vector是为了性能而创建的,他们并不保证安全,第一个使用了mutex和cond_pro,cond_cus来实现任务队列安全和同步,第二个使用了信号量sem_t lock,sem_t idle,sem_t data,step_read,step_write来实现任务队列的安全和同步。
3、为了生产者生产的资源能够进入任务队列,为了消费者能够消耗资源,在任务队列里边还实现了push和pop。实现这两个函数的时候注意安全性。注意锁和条件变量的使用。
总结:所以在任务队列的类里边有私有成员变量,构造函数,析构函数,push,pop
对于主函数main和生产者消费者入口函数的总结
1、在main函数构造一个任务队列类的对象,然后创建消费者生产者的多个线程(消费者线程一个for循环,生产者线程一个for循环,然后线程等待一个for循环)。由于消费者生产者都得对任务队列进行操作,任务类对象又是局部变量,所以将任务类对象作为入口函数的参数传递过去。传递的时候注意类型的强转。
2、注意多个线程要么在入口函数detach或者在主函数main中join
3、在生产者消费者的入口函数中,利用传过去的任务队列类对象调用push传入资源,利用pop取出资源。这里线程应该是无限的生产消费,所以是while(1)
信号量的注意点:
信号量能实现同步(sem_t 空闲 多个资源数+等待队列)
互斥(sem_t lock 一个资源数+等待队列)。
信号量与条件变量不同:
信号量通过自身的计数器来实现判断,不需要搭配互斥锁。
条件变量需要程序员自己进行判断,需要搭配互斥锁使用。
信号量与互斥锁的不同
信号量可以实现互斥,但更多实现的是同步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值