linux --线程(五)线程池模型

线程池(thread pool)

一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
例如,线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。

应用范围
1、需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
2、对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
3、接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,并现"OutOfMemory"的错误。

线程池组成部分

1、线程池管理器(ThreadPoolManager):用于创建并管理线程池
2、工作线程(WorkThread): 线程池中的线程
3、任务接口(Task): 每个任务必须实现的接口(回调函数),以供工作线程调度任务的执行。
4、任务队列 : 用于存放没有处理的任务。提供一种缓冲机制。
在这里插入图片描述
如何让相同入口函数的线程,处理不同的请求
1.switch case语句 : 但是处理大量不同需求的时候,case会太多造成麻烦。
2.向线程池抛入数据的时候,将处理该数据的函数一起抛入(函数地址) , 线程池当
中的线程只需要调用传入的函数处理传入的数据即可;

线程池生产者消费者模式示例

线程池生产者消费者模式,是比较常见的实现方式,比较简单。分为同步层、队列层、异步层三层。同步层的主线程处理工作任务并存入工作队列,工作线程从工作队列取出任务进行处理,如果工作队列为空,则取不到任务的工作线程进入挂起状态。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <iostream>
#include <queue>

//线程池 = 线程安全队列 + 一大堆的线程
//线程安全队列
//    元素 = 数据 + 处理数据的函数地址

//- 创建固定数量的线程池,然后循环从任务队列中获取任务对象
//- 获取任务对象后,然后执行任务对象中的任务接口
#define THREADCOUNT 4

typedef void* (*Handler_t)(int);//回调函数类型,线程任务接口

class ThreadTask //任务队列存储自定义元素类型
{
    public:
        ThreadTask(int data, Handler_t handler)
        {
            data_ = data;//传入数据
            handler_ = handler;//传入处理数据的函数指针
        }

        ~ThreadTask()
        {
            //
        }

        //使用函数,处理数据
        void Run()
        {
            handler_(data_); 
        }
    private:
        int data_;
        Handler_t handler_; //返回值为void* 参数为int
};
//线程池结构
class ThreadPool
{
    public:
        ThreadPool()
        {
            capacity_ = 10;//任务队列容量
            thread_capacity_ = THREADCOUNT;//线程数
            pthread_mutex_init(&lock_, NULL);//初始化用于线程安全的互斥量
            pthread_cond_init(&cond_, NULL);//初始化同步的条件变量

            //创建线程
            bool is_create = ThreadCreate();
            if(!is_create)
            {
                printf("Threadpool Create thread failed\n");
                exit(1);
            }
            IsExit = false;//定义一个bool标志位来判断线程是否退出,注意也是一个临界资源,操作时要保证互斥

            cur_thread_count_ = THREADCOUNT;//闲置的线程数
        }

        ~ThreadPool()
        {

        }

        bool Push(ThreadTask* tt)//往线程池里的任务队列里 放数据和任务接口
        {
            pthread_mutex_lock(&lock_);
            if(IsExit) 
            {//线程已推出 ,无法再放任务
                pthread_mutex_unlock(&lock_);//注意返回前要解锁资源
                return false;
            }
            que_.push(tt);//数据元素入队
            pthread_mutex_unlock(&lock_);

            //当插入数据之后 通知线程池当中的工作线程(消费者),完成同步
            pthread_cond_signal(&cond_);
            return true;
        }

        bool Pop(ThreadTask** tt)//取队列里的元素
        {
            *tt = que_.front();
            que_.pop();
            return true;
        }
        /* 
        问题:从队列当中拿数据和处理业务数据的逻辑要是混合在一起了
		假设如果处理数据特别漫长,而只有一个线程在处理数据,而其他线程在等待获取队列当中元素,显然不是使用线程池想要的结果,所以采用的方式:将从队列当中拿数据和处理业务数据解耦开来
		    加锁的时候:只需要保证拿数据的时候是互斥的
		    处理业务数据的时候,多个线程可以并行的去运行
		 */
        void ThreadJoin()//等待线程退出,回收资源
        {
            for(int i = 0; i < THREADCOUNT; i++)
            {
                pthread_join(tid_[i], NULL);
            }
        }

        // 如果直接退出线程,则有可能队列当中当中还有数据没有处理完毕;
        // 我们不应该去调用这样的接口来结束我们的线程
        void ThreadExit()
        {
            for(int i = 0; i < THREADCOUNT; i++)
            {
                pthread_cancel(tid_[i]);//线程终止
            }
        }

        void ThreadPoolClear()
        {
            //标志位
            pthread_mutex_lock(&lock_);
            IsExit = true;//线程池里的线程可以退出了
            pthread_mutex_unlock(&lock_);

            if(cur_thread_count_ > 0) //如果还有闲置的线程
            {
                pthread_cond_broadcast(&cond_);//唤醒等待,去判断队列是否为空,进而,会走到判断条件,判断是否可以退出
            }
        }

  private:
        static void* ThreadStart(void* arg)//线程函数只有一个参数,所以使用static声明
        {
            ThreadPool* tp = (ThreadPool*)arg;
            while(1)
            {
                //从队列当中获取数据,进行消费 对于不同的线程而言,在获取数据的时候,是互斥的
                pthread_mutex_lock(&tp->lock_);
                ThreadTask* tt;
                while(tp->que_.empty())//队列里没有资源,判断是否可以安全退出
                {
                    //判断是否可以进行结束
                    if(tp->IsExit)//判断退出条件
                    {
                        //退出
                        tp->cur_thread_count_--;
                        pthread_mutex_unlock(&tp->lock_);
                        pthread_exit(NULL);
                    }
                    //不退出
                    //调用条件变量 等待接口
                    pthread_cond_wait(&tp->cond_, &tp->lock_);
                    //进入条件变量的pcb等待队列,等待被唤醒
                }
                //有资源,则先取队列里的数据元素
                tp->Pop(&tt); //传入指针的地址,出参
                pthread_mutex_unlock(&tp->lock_);
///保证取数据时是互斥的,而线程处理可以多个线程并发
                //调用队列当中元素提供的函数,去处理数据 对于线程走到该位置的时候,就可以并行的处理了
                tt->Run();//拿到队列元素后,调用任务接口
                //防止内存泄漏
                delete tt;
            }
        }

        bool ThreadCreate()
        {
            for(int i = 0; i < THREADCOUNT; i++)
            {
                int ret = pthread_create(&tid_[i], NULL, ThreadStart, (void*)this);
                if(ret != 0)
                {
                    perror("pthread_create");
                    return false;
                }
            }
            return true;
        }
    private:
        std::queue<ThreadTask*> que_;/线程池的存放资源和任务接口的队列
        size_t capacity_;//资源缓冲容量

        //互斥
        pthread_mutex_t lock_;
        //同步 消费线程的条件变量,但是并没有生产线程的条件变量
        //由于我们所说的,客户端的请求行为我们是无法控制的。所以就不需要通知生产者来进行生产,当生产线程有了数据,就直接往线程池当中抛入就可以了,在通知消费线程来进行消费
        pthread_cond_t cond_;//只需要一个条件变量即可

        //线程池当中的初始化的时候线程数量
        size_t thread_capacity_;

        //标识具体还有多少线程数量
        size_t cur_thread_count_;

        //保存线程池当中的线程的线程标识符
        pthread_t tid_[THREADCOUNT];

        //标志是否可以退出
        bool IsExit;
};

void* DealData(int data)//线程池里的里任务队列里数据元素里的任务接口
{
    printf("consume data is %d\n", data);
    return NULL;
}

int main()
{
    ThreadPool* tp = new ThreadPool();
    //在这个代码当中main函数的线程,就充当生产线程,往线程池的线程安全队列当中push数据
    for(int i = 1; i <= 50; i++)
    {
        ThreadTask* tt = new ThreadTask(i, DealData);//理解为封装数据和任务接口
        tp->Push(tt);//再传入任务队列,消费线程拿到后实现任务逻辑
    }

    //等待线程池当中线程退出
    sleep(15);
    tp->ThreadPoolClear();//线程池里的线程退出
    tp->ThreadJoin();//回收线程池里的线程退出后的资源
    delete tp;
    return 0;
}

线程池里的线程何时退出
退出问题:
要是线程直接退出,但是线程池当中线程安全队列里面可能还有数据没有处理,即对于客户端的请求没有应答问题。
所以要仔细判断线程池里的线程的几种状态:
线程池当中线程可能存在的几种情况
1.加互斥锁
–》加锁–》判断队列是否为空 --》IsExit 为true–》pthread_ exit
2.调用pthread_ cond. wait当中阻塞在pthread_ cond. wait接口当中
由于阻塞在等待接口当中,要退出时,将其唤醒,也还是要判断队列里是否有资源可以访问,即while–》加锁–》判断队列是否为空–》IsExit–》pthread_exit
3.在队列当中获取数据获取成功–》处理数据–》- while --》加锁–》判断队列是否为空–》IsExit --》 pthread_ exit
4.正在处理队列里面的数据》处理数据— while --》加锁–》判断队列是否为空–》IsExit --》 pthread. exit

在这里插入图片描述
结论:只有当线程判断了队列当中没有数据的情况下才可以退出。

//由于本代码里,我们知道会有多少资源进队列,但是事实上客户端的请求是不确定的,所以要加标志位判断来使线程退出
	//线程处理函数里的判断条件		 
			 if(tp->IsExit)//判断退出条件
             {
                    //退出
                    tp->cur_thread_count_--;
                    pthread_mutex_unlock(&tp->lock_);
                    pthread_exit(NULL);
             }
//控制线程推出
void ThreadPoolClear()
        {
            //标志位
            pthread_mutex_lock(&lock_);
            IsExit = true;//线程池里的线程可以退出了
            pthread_mutex_unlock(&lock_);

            if(cur_thread_count_ > 0) //如果还有可能等待的线程
            {
                pthread_cond_broadcast(&cond_);//唤醒等待,去判断队列是否为空,进而,会走到判断条件,判断是否可以退出,进而退出
            }
        }
原创文章 59 获赞 170 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_44785014/article/details/105790198