【Linux】——手撕线程池、简易线程池


前言

  在现代计算机编程领域,随着多核处理器和分布式系统的广泛应用,并发编程变得越来越重要。而线程作为实现并发的基本手段之一,其重要性不言而喻。然而,在实际的应用开发中,如何高效地管理和调度线程,以充分利用系统资源并提高程序的性能,一直是一个具有挑战性的问题。

  线程池作为一种被广泛应用的解决方案,为解决这一难题提供了一种优雅而有效的方法。它通过预先创建一定数量的线程,并合理地分配和管理任务,避免了频繁创建和销毁线程所带来的开销,同时也能更好地控制和协调线程的执行,从而显著提高程序的并发性能。

  本文将深入探讨线程池的概念、原理、应用场景以及实现细节,帮助读者全面了解线程池的相关知识,并能够在实际项目中灵活运用线程池来优化程序性能。


1.池化技术

池化技术是一种资源管理策略,它通过预先创建和缓存一定数量的资源(如数据库连接、线程、对象等),并在需要时从池中获取资源,使用完毕后将资源放回池中,以便下次复用。

池化技术的目的是提高资源的利用率,减少资源的创建和销毁开销,从而提高系统的性能和效率。

常见的池化类型:线程池、对象池等

2.线程池

线程池是池化技术的一种具体应用,是一种管理和复用线程的机制。

在传统的编程模式中,每当有任务需要执行时,就会创建一个新的线程来处理该任务。当任务完成后,线程就会被销毁。

频繁地创建和销毁线程会带来较大的开销,包括系统资源的分配和回收、线程上下文的切换等。

线程池则预先创建了一定数量的线程,并将它们维护在一个池子中。

当有任务到来时,从线程池中获取一个空闲线程来执行任务;当任务完成后,线程不会被销毁,而是放回线程池中,等待下一次任务的分配。

2.1线程池的工作流程

线程池的工作流程可以归纳为以下四个步骤

任务提交
线程分配
执行任务
线程回收

1.任务提交

  • 当有新的任务需要执行时,应用程序将任务提交给线程池。线程池会根据当前线程池的状态和配置策略来决定如何处理该任务。

2.线程分配 

  • 如果线程池中有空闲线程,那么直接将任务分配给该线程执行
  • 如果没有空闲线程,且当前线程数量未达到线程池的最大限制,那么会创建一个新的线程来执行任务
  • 如果线程数量已经达到最大限制,且任务队列已满,那么根据线程池的拒绝策略来处理该任务,常见的拒绝策略包括直接丢弃任务、抛出异常等

3.任务执行

  • 线程从线程池中获取到任务后,开始执行任务。在执行过程中,线程池会对线程进行监控和管理,确保任务的正常执行

4.线程回收

  • 当任务执行完成后,线程不会被销毁,而是放回线程池中,等待下一次任务的分配

3.标准线程池的实现

了解了什么是线程池,现在就来实现一下线程池

首先需要一个线程类,该类包含线程的基本信息

  • 线程tid
  • 线程数据
  • 执行函数

以下是Thread类的具体实现

thread.hpp

 struct ThreadData{
        void* _args;
        std::string _name;
        ThreadData(std::string& name,void* args)
        :_name(name)
        ,_args(args)
        {}
    };
    typedef void* (*fun_t)(void*);//线程要执行的函数的参数和返回值都为void*
    class Thread{
    private:
        fun_t func;       // 线程要执行的函数
        ThreadData _data; // 线程的名字和线程的数据
        pthread_t tid;    // 线程的tid
    public:
        Thread(std::string& name,fun_t function,void * args)
        :func(function)
        ,_data(name,args)
        {}
        //启动一个线程
        void start(){
            pthread_create(&tid,nullptr,func,_data._args);
        }
        //等待指定线程
        void join(){
            pthread_join(tid,nullptr);
        }
        std::string& name(){
            return _data._name;
        }
        ~Thread(){}
    };

线程池中会将任务放入任务队列当中,每个线程都会从任务队列中获取任务

多线程并发访问同一个任务队列,任务队列是共享资源,因此需要锁来保护任务队列

我们平常使用锁对资源进行保护,需要两个操作

  • 加锁——pthread_mutex_lock
  • 解锁——pthread_mutex_unlock

每次都需要两行语句,很麻烦

有没有什么简单、不吃操作的办法

很简单,实现一个LockGuard类,让其实现自动加锁、解锁

  • 创建这个类对象时,自动加锁
  • 析构这个类对象时,自动解锁

以下是LockGuard类的具体实现

LockGuard.hpp

class Mutex{
    private:
        pthread_mutex_t* _mtx;
    public:
        Mutex(pthread_mutex_t* mtx):_mtx(mtx){}
        void lock(){
            pthread_mutex_lock(_mtx);
        }
        void unlock(){
            pthread_mutex_unlock(_mtx);
        }
        ~Mutex(){}
    };
    //构造时加锁,
    //出作用域时,析构时解锁
    //一键实现自动加锁和解锁
    class LockGuard{
    private:
          Mutex _mtx;
    public:
        LockGuard(pthread_mutex_t* mtx):_mtx(mtx){
            _mtx.lock();
        }
        ~LockGuard(){
            _mtx.unlock();
        }

    };

准备好了线程类和LockGuard类,则就是线程池类的实现

实现线程池,紧扣以下要求即可

  1. 提交任务
  2. 线程获取任务

盯着这两点,再进行扩展。

以下是线程池类的实现

ThreadPool.hpp

 template<class T>
    class ThreadPool{
    private:
        std::vector<Thread*> _threads;//用于存储找到所有线程的表
        int _num;//线程的数量
        std::queue<T> _task_queue;//任务队列
        static ThreadPool<T>* _pool_ptr;//线程池管理句柄,一种线程池只能出现一次
        static pthread_mutex_t _mtx;
        pthread_mutex_t lock;
        pthread_cond_t cond;
        bool stop=false;
    public:
        pthread_mutex_t* getMutex(){
            return &lock;
        }
        bool isEmpty(){
            return _task_queue.empty();
        }
        void waitCond(){
            pthread_cond_wait(&cond,&lock);
        }
        T getTask(){
            T task = _task_queue.front();
            _task_queue.pop();
            return task;
        }
    private:
        ThreadPool(int num):_num(num){
            pthread_mutex_init(&lock,nullptr);
            pthread_cond_init(&cond,nullptr);
            for(int i=1;i<=_num;i++){
                std::string name="Thread"+std::to_string(i);
                _threads.push_back(new Thread(name,runtine,this));
            }
        }
    public:
        ~ThreadPool(){
            stop = true;
            pthread_cond_broadcast(&cond);
            for(auto& x: _threads){
                x->join();
                delete x;
            }
            pthread_mutex_destroy(&lock);
            pthread_cond_destroy(&cond);
        }
        //单例化模式
        static ThreadPool<T>* getThreadPoolPtr(int num){
            if(_pool_ptr == nullptr){
                LockGuard tmp(&_mtx);
                if(!_pool_ptr){
                    _pool_ptr = new ThreadPool<T>(num);
                }
            }
            return _pool_ptr;
        }
        void push_task(const T& task){
            LockGuard tmp(&lock);//提交任务
            _task_queue.push(task);
            pthread_cond_signal(&cond);//唤醒一个线程
        }
        void run(){
            for(auto& x: _threads){
                x->start();
                std::cout<< x->name()<<"启动成功"<<std::endl;
            }
        }
        static void* runtine(void* arg){
            ThreadData* data = (ThreadData* ) arg;
            ThreadPool<T>* ptr = (ThreadPool<T>*)data->_args;
            while(true){
                T task;
                {
                    LockGuard tmp(ptr->getMutex());
                    while (ptr->isEmpty())
                    {   if(stop) return nullptr;
                        ptr->waitCond();
                    }
                    if(stop) return nullptr;
                    task = ptr->getTask();
                    //执行任务
                }
            }

        }
    };
    template<typename T>
    ThreadPool<T>* ThreadPool<T>::_pool_ptr = nullptr;
    template<typename T>
    pthread_mutex_t ThreadPool<T>::_mtx = PTHREAD_MUTEX_INITIALIZER;

4.简易线程池

上面的标准线程池实现起来是很麻烦的,且细节会更多

当有人让你现场手撕的时候,你不一定能写出来,就算写出来,也会浪费很多时间

能展现你的实力,且不会占用太多时间,我们实现一个简易的线程池即可

简易线程池的实现

class ThreadPool{
    private:
        int _num; // 线程数量
        static ThreadPool<task>* _ptr;
        std::vector<int> threads;
        static pthread_mutex_t* mutex;
        pthread_mutex_t *_mtx;
        pthread_cond_t *_cond;
        std::queue<tsak> _tasks;

    private:
        ThreadPoll(int num) : _num(num)
        {
            pthread_mutex_init(_mtx);
            pthread_cond_init(_cond);
        }
        ThreadPool(ThreadPool &another) = delete;
        ThreadPool &operator=(ThreadPool &another) = delete;
    public:
        ThreadPool<task>* getThreadPoolPtr(int num){
            if(_ptr == nullptr){
                pthread_mutex_lock(mutex);
                if(_ptr == nullptr){
                    _ptr = new ThreadPoll<task>(num);
                }
                pthread_mutrx_unlock(mutex);
            }
            return _ptr;
        }
        void push(task value){
            pthread_mutex_lock(_mtx);
            _tasks.push(value);
            pthread_mutex_unlock(_mtx);
            pthread_cond_signal(_cond);
        }
        bool isEmpty(){
            return _tasks.size()==0?true:false;
        }
        void  getTask(task& t){
             t = queue.front();
             queue.pop();
        }
        void* runFunction(void* arg){
            ThreadPoll<task>* ptr = (ThreadPoll<task>* )arg;
            while(true){
                task T;
                pthread_mutex_lock(_mtx);
                while(ptr->isEmpty()){
                    pthread_cond_wait(_cond,_mtx);
                }
                ptr->getTask(T);
                pthread_mutex_unlock(_mtx);
                //
            }
        }
        void start(){
            for(int i=0;i<_num;i++){
                pthread_t tid = 0;
                pthread_create(&tid,nullptr,runFunction,(void*)this);
                threads.push_back(tid);
            }
        }
    
};
template< class task>
ThreadPool<task>* ThreadPool<task>::_ptr = nullptr;
template< class task>
pthread_mutex_t* ThreadPool<task>::mutex = PTHREAD_MUTEX_INITIALIZER;

结语

  在本文中,我们一同探索了线程池的奇妙世界,了解了它的工作原理、优点以及具体的实现方法。线程池就像是一个神奇的“工厂”,高效地管理和调度着线程资源,让它们有条不紊地完成各项任务。

  在结束这份探索之旅时,你可能会思考一些更深层次的问题。比如,如何根据不同的业务需求选择最适合的线程池参数?在高并发环境下,如何进一步优化线程池的性能?这些问题都需要我们在实践中不断探索和总结。希望大家在今后的学习和工作中,能够继续深入研究线程池,将其运用到更多的场景中,为解决实际问题提供更多的思路和方法。