生产者与消费者模型的应用:线程池

生产者消费者模型

生产者与消费者模型:大佬们针对典型的应用场景设计的解决方案
生产者与消费者模型应用场景:有线程不断地产生数据,有线程不断地处理数据。
数据的产生与数据的处理,如果放在同一个线程中完成,因为执行流只有一个,那么肯定是生产一个处理一个,处理完一个后才能生成一个。
这样的依赖关系太强了,如果处理的比较慢,就会把程序的速度拖慢下来。
因此将生产与处理放在不同的执行流中完成,那么生产和处理的线程如何实现通信呢?
中间增加一个数据缓冲区(队列),作为中间的数据缓冲场所。
产生线程只负责将数据放入缓冲区,处理数据只负责将数据从缓冲区中取出并处理。
在这里插入图片描述

生产者消费者模型的优点

  • 解耦合

    将模块分开,耦合度降低两边都会比较灵活,各自可以按照各自的业务压力创建线程处理,
    且处理模块代码发生改变只需要改变处理模块代码。

  • 支持忙闲不均

    缓冲区大小可以一直申请,是可调的,数据多了就可以放在缓冲区。

  • 支持并发

    可以有多个执行流进行处理

生产者消费者模型的优点很显然,生活中很多时候是请求多于响应的,根据这个模型,大佬们就设计了线程池。

线程池

一个或多个创建好的线程+线程安全pcb等待队列
其他线程将要处理的任务,添加到线程池任务队列中,线程池的线程不断从pcb等待队列中获取任务进行处理。
应用:应用于有大量请求任务需要处理的场景

	优点:
		避免了峰值压力下资源耗尽的风险
		避免了大量频繁创建与销毁线程带来的时间成本
		正常流程:任务处理T = 创建线程T1 + 任务处理过程 + 销毁线程T3
				这样的流程大量的时间都放在了创建和销毁线程,没有把所有的资源都放在任务处理上

实现:一个或多个创建好的线程 + 线程安全的任务队列
如何知道任务如何处理:

1.在线程入口函数中直接定义好任务处理逻辑 
    错误的理解
	这样写死的线程池只能处理特定的功能,达不到处理各式各样功能的目的
2.添加任务的线程,添加任务是,将要处理的数据和数据处理的方法一并传入线程池
	线程池中的线程只需要调用处理函数传入数据即可
	task{
		void* data;
		void(*handler)(void* data);
	}
	线程池中的线程只需要调用一下  handler(&data);就可以完成处理数据完成任务

设计一个简单的线程池

设计任务类:
当线程池的线程获取到任务对象时,只需要调用一下它的Run即可完成任务

//定义一个函数指针
typedef void(*handler_t)(int)
class ThreadTask
{
	private:
		int _data;    //要传入的数据
		handler_t _handler; //处理数据的方法
	public:
		void Run()
		{
			_handler(data);
		}
}

设计一个线程安全的任务 队列:
主要就是实现同步与互斥,这点可以使用信号量,可以使用条件变量都可以。
本文以条件变量实现为例:

#define MAXCAP 3    
class BlockQueue    
{    
 private:    
   std::queue<ThreadTask> q;    
   size_t cap;    
   pthread_mutex_t mutex;    
   pthread_cond_t Cus_cond;    
   pthread_cond_t Pro_cond;    
 public:    
   BlockQueue(size_t c = MAXCAP):cap(c)    
 {    
   //mutex=PTHREAD_MUTEX_INITIALIZER;      //C++不用这个    
   pthread_mutex_init(&mutex,NULL);    
   pthread_cond_init(&Cus_cond,NULL);    
   pthread_cond_init(&Pro_cond,NULL);    
 }    
   ~BlockQueue()                                                                                             
   {    
     pthread_mutex_destroy(&mutex);    
     pthread_cond_destroy(&Cus_cond);    
     pthread_cond_destroy(&Pro_cond);    
   }    
   bool push(const ThreadTask& data)
   {
     //要往缓冲区队列放数据,就得先判断缓冲区是否满了,所有线程都可以访问缓冲区,那么缓冲区就是临界资源
     //需要进行保护
     pthread_mutex_lock(&mutex);
     //循环条件防止时间片调度产生的访问出错
     while(q.size() == cap)
     {
       pthread_cond_wait(&Pro_cond,&mutex);//解锁阻塞加锁
     }
     q.push(data);
     pthread_cond_signal(&Cus_cond);
     pthread_mutex_unlock(&mutex);
     return true;
   }
   bool pop(ThreadTask* data)
   {
     pthread_mutex_lock(&mutex);                                                                             
     while(q.empty() == true)
     {
       pthread_cond_wait(&Cus_cond,&mutex);
     }
     *data= q.front();
     q.pop();
     pthread_cond_signal(&Pro_cond);
     pthread_mutex_unlock(&mutex);
     return true;
   }
};

可以自行实现一个,加上模板最好。
信号量版本
点击超链接,我在另一篇文章写到过,不再赘述。
实现一个线程池类
线程池类只需要创建几个线程不停地获取任务即可,所以

成员设计:
	1.最大线程数  -- 决定创建几个线程    _max_thread
	2.一个线程安全的任务队列,  BlockQueue    
成员方法:
	3.其他线程将任务入队,TaskPush(const ThreadTask& task);
	4.构造方法,创建指定数量的线程
	5.线程入口函数,出队获取任务,调用task.Run()
#define MAX_THREAD 5
#define MAX_QUEUE 10
class threadpool
{
  private:
    int _max_thread;                                                                                          
    BlockQueue _queue;
  private:
    //这个入口函数如果在类中声明,就会自动带有this指针,而入口函数的参数返回值是固定的,将他定义为static
    //就可以解决,但static后就变成了全局的,相当于每个线程调的都是这一个入口函数,也就达不到目的了     
    //将this通过参数传进来就可以解决问题                                                          
    static void* th_entry(void* arg)    
    {                               
      threadpool*pb = (threadpool*)arg;
      while(1)                         
      {       
        ThreadTask task;
        pb->_queue.pop(&task);   //获取到节点
        task.Run();                          
      }            
      return NULL;
    }
  public:
    threadpool(int maxt = MAX_THREAD,int maxq = MAX_QUEUE):_max_thread(maxt),_queue(maxq)
    {
      int ret;
      pthread_t tid[];
      for(int i = 0; i<_max_thread; ++i)
      {
        ret = pthread_create(&tid[i],NULL,th_entry,this);
        if(ret != 0)
        {
          exit(-1);  //构造函数没法判断成功与否,出问题就直接退出进程
        }
        pthread_detach(tid[i]);
      }
    
    }
    bool TaskPush(const ThreadTask& task)                                                                     
    {
      return _queue.push(task);
    }
};

线程池类设计注意点:
需要注意的是,线程的入口函数,在C语言中明确定义了格式,为void* entry(void*),必须是一个void类型的参数和void返回值,这是不能改变的,在类中创建线程,入口函数也要写在类中,但问题就来了。
类中的成员方法自带this指针,而这样函数参数多了一个,系统就检测不到这是入口函数,报错:线程入口函数未定义。想要解决,就需要在类外定义,这样未免太过麻烦。直接加上static好了,这样就不带this指针了。
但是问题又来了,我们知道,通过this指针才能确保线程调用自己对应的入口函数,如果没了this指针,而且,static修饰方法,这样一来所有的线程都掉用的是同一个entry了。我们如何确保线程调用的是自己的入口函数呢?
解决办法就是,将this指针作为参数在线程创建的时候传入入口函数,这样问题都解决了。
解决方法:static修饰入口函数 + this作参数

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值