【Linux】线程池以及不同场景下的线程池的设计

线程池:

概念
线程的池子,有很多的池子,但是数量不会超过池子的限制。需要用到多执行流进行多任务处理的时候,就从池子中取出一个线程去处理

优点
有大量的数据处理请求,需要多执行流并发/并行处理。若是一个数据请求的到来伴随着一个线程的创建去处理,则会产生一些风险以及不必要的消耗:

  1. 线程若不限制数量的创建,在峰值压力下,线程创建的过多,资源耗尽,有程序崩溃的风险
  2. 处理一个任务的时间: 创建线程的时间t1 + 任务处理时间t2 +线程销毁时间t3 = T,
    若t2/T比例 占据不够高,则表示大量的资源用到线程的创建与销毁上,因此线程池使用已经创建好的线程进行循环处理,就避免了大量的线程的频繁创建与销毁的时间成本。

线程池的实现条件
大量的线程(每个线程中都进行循环的任务处理) + 任务缓冲队列

任务缓冲队列的任务被多个线程访问处理,每一个线程处理一个任务
在这里插入图片描述

在设计线程池上的存在的问题以及解决:

灵活性太差。线程的入口函数,都是在创建线程的时候就固定传入的,导致线程池中的线程进行任务处理的方式过于单一。因为线程的入口函数都是一样的(都是void*参数类型),处理流程都是一样的,只能单一的处理单一方式的请求。

解决方法:
所以,我们的线程池要将任务和任务的处理方法全部调价到任务队列中。也就是用户传入任务的时候,不仅仅传入任务,还要传入任务的处理方法。这样就知道改用什么样的方法处理什么样的任务。
线程池中的线程不用关注用什么方法去处理你的任务,只用关注用你的方法处理你的任务,也就是只是用你的方法处理你的任务。
可以形象的比喻为: 线程是搬砖的,但是他不用管如何建造房子,它只用将砖搬到你指定的位置

为什么要这样做?
线程池中的线程只管处理,但是如何处理不管,由处理方法决定,处理方法改变了,线程之不需要改动,降低耦合度。
在这里插入图片描述

线程池的实现框架:

线程池的实现框架:分为任务类线程池类

任务类的功能:将数据和数据的处理方法定义好之后,通过任务类将数据和数据的处理方法传参的形式传给任务类(SetTask函数绑定数据和数据执行的方法函数),在外部只需要调用Run接口就可以实现特定的数据调用特定的方法。

线程池类的功能
将任务加载到任务等待的队列中,调用线程的入口函数,使得每一个线程不断的从任务队列中取出任务,执行任务的接口Run,完成特定的数据调用特定的任务。
线程池的任务等待队列上是一个个MyTask的任务,这些任务的数据和函数已经通过SetTask组织成一个节点,每一次线程到任务队列上,拿出一个节点,直接调用Run接口完成任务处理。

typedef void(*_handler)(int data)
class MyTask   //任务类
{
 private:
int _data; //处理数据
handler_t _handler //处理数据的方法
public:
SetTask(int data, handler_t handler); //用户自己传入要处理的数据和方法,组织出一个任务节点
Run{return _handler(_data);} //执行这一任务
}

class ThreadPool
{
public:
bool TaskPush(MyTask & task); // 将任务放进任务缓冲队列中
void * thr_start(void* arg);  // 取出任务节点,并且调用Run节点进行处理任务节点

private:
int thr_max ; //定义线程池中的线程的最大数量-初始化时创建相应数量的线程即可
queue<MyTask> _queue; 
pthread_mutex_t _mutex ; // 实现_queue操作的安全性
pthread_mutex_t _cond; //实现线程池中消费者线程的同步
}

注意事项:

  1. 每一个线程的入口函数中,不断的获取任务节点,调用任务节点中的Run接口就可以实现任务的处理了。
  2. 线程的任务处理(函数)是在线程池的外部自己定义的,它的修改不会影响到线程池,降低耦合度。
  3. handler函数(任务处理)是动态的,不是统一的,根据数据的改变而改变,但是线程的入口函数(参数是void*)必须是固定的。

对于每一个要处理的数据直接调用handler函数处理就可以了,为什么要封装一次Run呢?
线程初始化的时候就要创建线程,但是这个时候还没有任务,不确定哪一个任务用哪一个handler解决方法,因为一旦直接调用handler,就只能调用这个handler处理任务了,没有办法处理其他类型的任务,所以要封装起来。线程并不直接运行handler,它不知道handler是什么,线程初始化的时候还没有handler,还没有任务处理呢。所以,线程池中的线程只管调用Run,至于里面的handler是什么如何处理,那是用户的事情。提高灵活性。

基于IO密集型和CPU密集型的线程池如何设计

首先,我们要明白一点:
线程池中的线程是对于数据data和数据要完成的任务handler进行任务处理的。所以在不同类型的程序上,线程池大的框架是不变的。都是线程进行任务处理。这一点是毋庸置疑的。
我们所需要处理的是: 线程池在不同类型的程序上初始化的线程数量是多少,因为线程之间的切换如果耗时过大,则就会倒打一耙

CPU密集型:在一个任务中,CPU做不断的大量计算,持续的运行,CPU的利用率很高。
IO密集型:在一个任务中,需要进行两个步骤:IO等待与IO就绪(数据拷贝)。但是大部分的时间或者线程都在进行IO等待上,IO的速度远小于CPU密集型程序,CPU的利用率低。

对于CPU密集型程序的线程池设计:

线程个数为CPU核数 +1 。这几个线程可以并行执行,不存在线程切换到开销,提高了cpu的利用率的同时也减少了切换线程导致的性能损耗。同时也应对了当有一个线程处于阻塞状态时,这个线程可以去替代那一个阻塞的线程做CPU运算,如果再多的话,那么线程之间的切换就可能导致性能下降。

对于IO密集型程序的线程池设计:

线程个数为CPU核数的两倍。在其他线程IO操作的时候,其它线程可以继续持有CPU,提高CPU的利用率。同时,多个线程可以同时并行的进行IO等待,压缩等待时间,提高效率。在这同时,还可以高效的利用CPU。

线程池的实现:

  1 #include <iostream>                                                                                                                
  2 #include <queue>
  3 #include <pthread.h>
  4 #include <mutex>
  5 #include <stdio.h>
  6 #include <unistd.h>
  7 #include <stdlib.h>
  8 
  9 using namespace std;
 10                                                                                                                                    
 11 typedef void (*handler_t)(int); //线程的入口函数
 12 
 13 class ThreadTask
 14 {
 15  public:
 16    ThreadTask()
 17    {
 18 
 19    }
 20    void SetTask(int data, handler_t handler)
 21    {
 22         _data = data;
 23         _handler = handler;
 24    }
 25    void Run() //外部只需要调用Run,不用关心任务如何处理
 26    {
 27        return _handler(_data);
 28    }
 29  private:
 30    int _data; //多任务处理的数据
 31    handler_t _handler; // 任务重处理数据的方法
 32 
 33 };
 34  #define MAX_THREAD 10  //定义的线程池中线程的数量最大值
 35 class ThreadPool
 36 {
 37  public:
 38     ThreadPool(int max_thr = MAX_THREAD)
 39         :thr_max(max_thr)
 40     {
 41         pthread_mutex_init(&_mutex,NULL);
 42         pthread_cond_init(&_cond,NULL);
 43         for(int i = 0; i < thr_max; i++)
 44         {
 45             pthread_t tid;
 46             int ret = pthread_create(&tid,NULL,thr_start,this);
 47             if(ret != 0)
 48             {
 49                 printf("thread create error\n");
 50                 exit(-1); //构造函数无法通过返回值判断线程是否退出,所以exit
 51             }
 52 
 53         }
 54     }
 55     ~ThreadPool()
 56     {
 57         pthread_mutex_destroy(&_mutex);
 58         pthread_cond_destroy(&_cond);
 59     }
 60     bool TaskPush(ThreadTask & task)
 61     {
 62         pthread_mutex_lock(&_mutex);
 63         _queue.push(task);
 64         pthread_mutex_unlock(&_mutex);
 65         pthread_cond_broadcast(&_cond); // 对后唤醒的所有线程,谁抢到水处理
 66         return true;
 67     }
68 
 69     // 类的成a员函数,有一个隐藏的默认参数 this指针,
 70     // 线程的入口函数只能传一个参数 
 71     // 所以加上static成员函数,类共享的
 72     // 但是不能访问到类的非静态成员变量
 73     //
 74     static  void* thr_start(void* arg)
 75     {
 76         ThreadPool *p = (ThreadPool*)arg;
 77         //不断的从任务队列中取出任务,执行任务的接口Run
 78         while(1)
 79         {
 80             pthread_mutex_lock(&p->_mutex);
 81             while(p->_queue.empty())
 82             {
 83                 pthread_cond_wait(&p->_cond,&p->_mutex);
 84             }
 85             ThreadTask task;
 86             task = p->_queue.front();
 87             p->_queue.pop();
 88             pthread_mutex_unlock(&p->_mutex);
 89             // 如果将Run放在解锁前,就没有意义,相当于一个线程处理的时候其他线程还要等待处理完任务,
 90             // 这样的话,就相当于一个线程串行的完成任务
 91             task.Run();  //任务的处理要放在解锁之外,因为当前的锁保证的是队列的操作
 92         }
 93        
 94         return NULL;
 95     }
 96  private:
 97     int thr_max; //线程池中的线程的最大数量 --根据这个初始化创建指定线程
 98     queue<ThreadTask> _queue;
 99     pthread_mutex_t _mutex; //保护队列操作的互斥锁
100     pthread_cond_t _cond; // 实现从队列中获取节点的同步的条件变量
101 };
         

猜你喜欢

转载自blog.csdn.net/weixin_43939593/article/details/106516056