Linux多线程(六):线程池


一、池化技术

核心思想:预先创建并维护一组可重用的资源(称为“资源池”),当需要时从池中获取,使用完毕后归还,而不是每次都重新创建和销毁。

一个形象的比喻:把它想象成共享单车服务。

  • 没有池化:每次你需要用车时,自己去工厂买一辆车,用完后直接扔掉。
  • 使用池化:有一个单车停放点(资源池),里面有预先准备好的很多单车(资源)。你需要是就去租一辆,用完后归还到停车点。这样就极大地提高了单车的使用效率,避免了因频繁制造和销毁单车的巨大开销。

池化技术本质上就是通过“空间换时间”和“复用”来提升系统性能、稳定性和可管理性。

这个技术在计算机领域得到了广泛的应用,常见的有内存池、进程池、线程池……

二、线程池

线程池是一种线程的使用模式。线程过多会提高调度的开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。

线程池的优点

  • 避免了在处理短时间任务时创建与销毁线程的代价。
  • 线程池不仅能够保证内核的充分利用,还能防止过分调度。

:可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络socket等的数量。

线程池的应用场景

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

线程池的种类

  1. 固定线程池。线程数量是固定的,由我们手动设置,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务接口。
  2. 浮动线程池。线程数量可视情况动态变化的线程池。其他同上。

本文我们研究固定线程池。

三、实现线程池

下面我们实现一个简单的固定线程池,线程池中提供了一个任务队列,以及若干个线程(多线程)。
在这里插入图片描述
其实,线程池,本质上就是一个生产者消费者模型。

测试时,我们还是用之前测试生产者消费者模型的简单运算任务类:
task.hpp

#pragma once

#include <iostream>
#include <string>

std::string opers="+-*/%";

enum{
    DivZero=1,
    ModZero,
    Unknown
};

class Task
{
public:
    Task()
    {}
    Task(int x, int y, char op) : data1_(x), data2_(y), oper_(op), result_(0), exitcode_(0)
    {}

    void run()
    {
        switch (oper_)
        {
        case '+':
            result_ = data1_ + data2_;
            break;
        case '-':
            result_ = data1_ - data2_;
            break;
        case '*':
            result_ = data1_ * data2_;
            break;
        case '/':
            {
                if(data2_ == 0) exitcode_ = DivZero;
                else result_ = data1_ / data2_;
            }
            break;
        case '%':
           {
                if(data2_ == 0) exitcode_ = ModZero;
                else result_ = data1_ % data2_;
            }            break;
        default:
            exitcode_ = Unknown;
            break;
        }
    }

    void operator ()()
    {
        run();
    }

    std::string GetResult()
    {
        std::string r = std::to_string(data1_);
        r += oper_;
        r += std::to_string(data2_);
        r += "=";
        r += std::to_string(result_);
        r += "[code: ";
        r += std::to_string(exitcode_);
        r += "]";

        return r;
    }

    std::string GetTask()
    {
        std::string r = std::to_string(data1_);
        r += oper_;
        r += std::to_string(data2_);
        r += "=?";
        return r;
    }
    
    ~Task()
    {}

private:
    int data1_;
    int data2_;
    char oper_;

    int result_;
    int exitcode_;
};

接下来我们实现三个版本的线程池:

V1:基础版线程池

思路

  • 成员变量设计:
    • 采用vector容器作为线程池
    • 适配器queue作为任务队列
    • 由于我们实现的固定线程池,所以需要一个num来表示线程池的线程数量。
    • 任务队列会被多线程并发访问,需要用互斥锁维护线程安全。
    • 避免某些线程竞争能力过强,造成其他线程的饥饿问题,最好是让线程顺序访问,需要用到条件变量来维护线程同步。
  • 简单的封装原生线程库:
    • 其一:为了方便测试,我们通常需要为线程设置一些信息(线程名、线程id等),测试时打印这些信息可以直观展现多线程并发处理任务的结果。
    • 其二:简化线程控制操作
    • 我们仿造C++11的thread线程库来简单封装两个接口(线程创建和线程等待),也就是run和join。我们将线程名和线程id作为成员变量,描述线程信息。此外最重要的是run要传入线程入口的函数参数,我们在内部回调即可。
    • 具体代码如下(下文)
  • 封装互斥锁与条件变量的操作接口:
    • 这些接口名字又臭又长,不如自己起一个即形象又短的接口名。
  • 接口设计:
    • Push(生产):线程池需要让外部将任务Push到任务队列当中
    • Pop(消费):线程池需要从任务队列中获取任务并处理任务
      注意这个接口为线程池自己调用,不必暴漏给外部。
    • Init:初始化线程池,实例化线程池的线程对象,注意此时线程还并未真正创建。
    • Start:启动线程,真正意义上创建线程,线程池准备就绪(等待任务)。

代码如下

封装原生线程库Thread.hpp

#pragma once

#include <pthread.h>
#include <string>

typedef void (*callback_t)(void*);

class Thread
{
public:
    Thread(int num = 0, callback_t cb = nullptr, void *args = nullptr)
        : _tid(0), _cb(cb), _args(args), _threadname("Thread-" + std::to_string(num))
    {}

    void Run()
    {
        pthread_create(&_tid, nullptr, Routine, this);
    }

    void Join()
    {
        pthread_join(_tid, nullptr);
    }

    std::string GetName()
    {
        return _threadname;
    }
    
    pthread_t GetTid()
    {
        return _tid;
    }

private:
    static void *Routine(void *arg)
    {
        Thread *t = static_cast<Thread *>(arg);
        t->Entry();
        return nullptr;
    }

    void Entry()
    {
        _cb(_args);
    }

private:
    pthread_t _tid;            // 线程id
    std::string _threadname;   // 线程name
    callback_t _cb;            // 线程入口函数
    void *_args;               // 线程入口函数参数
};

线程池ThreadPool.hpp:

#pragma once

#include <iostream>
#include <pthread.h>
#include <vector>
#include <queue>
#include <string>
#include "Thread.hpp"
#include "task.hpp"

#define NUM 10 // 默认为10个线程

template<class T>
class threadpool
{
private:
     void Lock()
     {
          pthread_mutex_lock(&_mutex);
     }
     void UnLock()
     {
          pthread_mutex_unlock(&_mutex);
     }
     void WaitUp()
     {
          pthread_cond_signal(&_cond);
     }
     void ThreadSleep()
     {
          pthread_cond_wait(&_cond, &_mutex);
     }
     // 任务队列判空
     bool IsEmpty()
     {
          return _taskqueue.empty();
     }
     // 获取某个线程的线程名
     std::string GetName(pthread_t tid)
     {
          for(auto &t : _threads)
          {
               if(t.GetTid() == tid)
                    return t.GetName();
          }
          return "None!";
     }
public:
     threadpool(int num = NUM)
     :_num(num)
     {
          pthread_mutex_init(&_mutex, nullptr);
          pthread_cond_init(&_cond, nullptr);
     }

     // 实例化线程池的线程对象(注意此时线程还并未真正创建,需要start启动)
     void Init()
     {
          for(int i = 0; i < _num; ++i)
               _threads.push_back(Thread(i, HandlerTask, this));
     }

     // 线程的入口函数
     static void HandlerTask(void* args)
     {
          // 分离线程
          pthread_detach(pthread_self());

          threadpool<T> *tp = static_cast<threadpool<T> *>(args);
          std::string name = tp->GetName(pthread_self());
          while(true)
          {
               //消费任务
               tp->Lock();
               while(tp->IsEmpty())
                    tp->ThreadSleep();
               Task t = tp->Pop();
               tp->UnLock();
               
               //处理任务
               t();
               std::cout << name << "处理任务:" << t.GetTask() << " 结果为:" << t.GetResult() << std::endl;
          }
     }

     void Start()//启动线程池,准备就绪
     {
          for(int i = 0; i < _num; ++i)
               _threads[i].Run();
     }

     // 添加任务(生产任务),主线程完成
     void Push(const T &task)
     {
          Lock();
          _taskqueue.push(task);
          WaitUp();
          UnLock();
     }
     
     // 处理任务(消费任务),线程池线程完成
     T Pop()
     {
          T t = _taskqueue.front();
          _taskqueue.pop();

          return t;
     }

     ~threadpool()
     {
          pthread_mutex_destroy(&_mutex);
          pthread_cond_destroy(&_cond);
     }
private:
     std::vector<Thread> _threads;       // 线程池
     std::queue<T> _taskqueue;           // 任务队列
     int _num;                           // 线程池的线程数量
     pthread_mutex_t _mutex;             // 维护任务队列的互斥,一次只能一个线程来获取任务
     pthread_cond_t _cond;               // 维护线程的同步,未持有锁的线程在条件变量中阻塞,等待唤醒
};

代码解析

  1. 互斥锁与条件变量的使用注意事项
  • RALL,在构造函数和析构函数中初始化和销毁互斥锁和条件变量。
  • 当某线程被唤醒时,其可能是被异常或是伪唤醒,或者是一些广播类的唤醒线程操作而导致所有线程被唤醒,使得在被唤醒的若干线程中,只有个别线程能拿到任务。此时应该让被唤醒的线程再次判断是否满足被唤醒条件,所以在判断任务队列是否为空时,应该使用while进行判断,而不是if
  • 唤醒线程时最好使用pthread_cond_signal(代码封装为了WakeUp)函数唤醒正在等待的一个线程。而pthread_cond_broadcast函数的作用是唤醒条件变量下的所有线程,而外部可能只Push了一个任务,我们却把全部在等待的线程都唤醒了,此时这些线程就都会去任务队列获取任务,但最终只有一个线程能得到任务。一瞬间唤醒大量的线程可能会导致系统震荡,这叫做惊群效应。
  • 处理任务的动作应该在解锁之后进行,原因如下:当线程从任务队列中拿到任务后,该任务就已经属于当前线程了,与其他线程已经没有关系了,因此应该在解锁之后再进行处理任务,而不是在解锁之前进行。因为处理任务的过程可能会耗费一定的时间,所以我们不要将其放到临界区当中。
  • 如果将处理任务的过程放到临界区当中,那么当某一线程从任务队列拿到任务后,其他线程还需要等待该线程将任务处理完后,才有机会进入临界区。此时虽然是线程池,但最终我们可能并没有让多线程并行的执行起来。
  1. 线程池中线程的入口函数需要设置为静态成员函数

Start创建线程时需调用Run,需要为创建的线程传入一个线程入口函数HandlerTask,该函数只有一个参数类型为void*的参数,及返回值类型为void的参数(封装线程库时更改了)。

此时HandlerTask作为类的成员函数,该函数的第一个参数是隐藏的this指针,因此这里的HandlerTask函数,虽然看起来只有一个参数,而实际上它有两个参数(this、void*),此时直接将该HandlerTask函数作为线程的入口函数是不行的,无法通过编译。

于是我们将其设置为静态成员函数,它属于类,而并不属于某一个对象,静态成员函数没有隐藏的this指针,解决问题。

但是在静态成员函数内部却无法调用非静态成员函数,而我们在HandlerTask函数当中又需要调用该类的某些非静态成员函数,比如Pop。因此我在创建线程时,向HandlerTask函数传入的当前对象的this指针,此时我们就能够通过该this指针在HandlerTask函数内部调用非静态成员函数了。

主线程逻辑如下:
主线程实例化一个5个线程的线程池,初始化和启动后只负责不断向任务队列当中Push任务就行了,此后线程池当中的线程会从任务队列当中获取到这些任务并进行处理。

#include <iostream>
#include <unistd.h>
#include <ctime>
#include "Thread.hpp"
#include "ThreadPoolV1.hpp"
#include "task.hpp"

using namespace std;

int main()
{
    threadpool<Task> *tp = new threadpool<Task>(5);        // 实例化一个5个线程的线程池
    tp->Init();             // 初始化线程池
    tp ->Start();           // 启动(创建)线程,处理任务准备就绪
    srand(time(nullptr)/3); // 种随机数种子
    int len = opers.size();
    while(true)
    {
    		  // 生成计算任务
        int data1 = rand()%10 +1;
        usleep(10); // 隔开时间戳,避免前后随机数相近
        int data2 = rand()%10;
        char op = opers[rand()%len];
        Task t(data1, data2, op);
        // push任务到任务队列
        tp->Push(t);
        cout << "main thread add a task:" << t.GetTask() << endl;
        sleep(1); // 模拟生成任务需要的时间
    }

    delete tp;
    return 0;
}

运行结果:符合预期
在这里插入图片描述
此后我们如果想让线程池处理其他不同的任务请求时,我们只需要提供一个任务类,在该任务类当中提供对应的任务处理方法就行了。

V2:复用基于阻塞队列的生产者消费者模型的线程池

前面我们说过,线程池本质上就是一个生产者消费者的模型(生产者通过Push接口生产、消费者通过Pop接口消费、taskqueue就是交易场所)。既然我们曾经写过生产者消费者的模型,我们就试着复用一下基于阻塞队列的生产者消费者模型。

基于阻塞队列的生产者消费者模型代码如下:

block_queue.hpp:

#pragma once

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

template<class T>
class block_queue
{
    static const int defultnum = 20;
public:
    //RALL
    block_queue(int maxsize = defultnum):_maxsize(maxsize)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_c_cond, nullptr);
        pthread_cond_init(&_p_cond, nullptr);
    }

    T pop()
    {
        pthread_mutex_lock(&_mutex);
        while(_q.size() == 0)//使用while循环检测,是为了避免伪唤醒(不当的操作可能会唤醒一批线程,而我们只有一把锁),导致无锁的线程前去消费
        {
            pthread_cond_wait(&_c_cond, &_mutex);//阻塞等待,并释放锁;当被唤醒时,会去申请锁
        }
        T data = _q.front();
        _q.pop();
        pthread_cond_signal(&_p_cond);//消费后,唤醒一个生产者来生产
        pthread_mutex_unlock(&_mutex);
        
        return data;
    }

    void push(const T& data)
    {
        pthread_mutex_lock(&_mutex);
        while(_q.size() == _maxsize)
        {
            pthread_cond_wait(&_p_cond, &_mutex);
        }
        _q.push(data);
        pthread_cond_signal(&_c_cond);//生产后,唤醒一个消费者来消费
        pthread_mutex_unlock(&_mutex);
    }

    ~block_queue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_c_cond);
        pthread_cond_destroy(&_p_cond);
    }
private:
    std::queue<T> _q;          // 队列(共享资源)
    int _maxsize;              // 队列的最大值
    pthread_mutex_t _mutex;    // 锁
    pthread_cond_t _c_cond;    // 消费者条件变量
    pthread_cond_t _p_cond;    // 生产者条件变量
};

思路

  • 将taskqueue成员变量替换为blockqueue
  • 互斥锁和条件变量就不需要了,该模型已经维护好了多线程的同步与互斥。
  • Push和Pop接口直接封装该模型的push和pop即可
  • 其余的老样子,只需要维护好我们的线程池即可

代码如下
ThreadPoolV2.hpp:

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include "Thread.hpp"
#include "task.hpp"
#include "block_queue.hpp"

#define NUM 10

template<class T>
class threadpool
{
private:
     std::string GetName(pthread_t tid)
     {
          for(auto &t : _threads)
          {
               if(t.GetTid() == tid)
                    return t.GetName();
          }
          return "None!";
     }

     // 处理任务(消费任务),线程池线程完成
     T Pop()
     {
          T t = _bq.pop();
          return t;
     }
public:
     threadpool(int num = NUM)
     :_num(num)
     {}

     // 实例化线程池的线程对象(注意此时线程还并未真正创建,需要start启动)
     void Init()
     {
          for(int i = 0; i < _num; ++i)
               _threads.push_back(Thread(i, HandlerTask, this));
     }

     // 线程的入口函数
     static void HandlerTask(void* args)
     {
          // 分离线程
          pthread_detach(pthread_self());

          threadpool<T> *tp = static_cast<threadpool<T> *>(args);
          std::string name = tp->GetName(pthread_self());
          while(true)
          {
               //消费任务
               Task t = tp->Pop();
               //处理任务
               t();
               std::cout << name << "处理任务:" << t.GetTask() << " 结果为:" << t.GetResult() << std::endl;
          }
     }

     void Start()//启动线程池,准备就绪
     {
          for(int i = 0; i < _num; ++i)
               _threads[i].Run();
     }

     // 添加任务(生产任务),主线程完成
     void Push(const T &task)
     {
          _bq.push(task);
     }
     
     ~threadpool()
     {}
private:
     std::vector<Thread> _threads;       // 线程池
     int _num;                           // 线程池的线程数量
     block_queue<T> _bq;                 // 基于阻塞队列的生产者消费者模型
};

测试的主线程代码:

#include <iostream>
#include <unistd.h>
#include <ctime>
#include "Thread.hpp"
#include "ThreadPoolV2.hpp"
#include "task.hpp"

using namespace std;

int main()
{
    threadpool<Task> *tp = new threadpool<Task>(5);        // 实例化一个5个线程的线程池
    tp->Init();             // 初始化线程池
    tp ->Start();           // 启动(创建)线程,处理任务准备就绪
    srand(time(nullptr)/3); // 种随机数种子
    int len = opers.size();
    while(true)
    {
    		  // 生成计算任务
        int data1 = rand()%10 +1;
        usleep(10); // 隔开时间戳,避免前后随机数相近
        int data2 = rand()%10;
        char op = opers[rand()%len];
        Task t(data1, data2, op);
        // push任务到任务队列
        tp->Push(t);
        cout << "main thread add a task:" << t.GetTask() << endl;
        sleep(1); // 模拟生成任务需要的时间
    }

    delete tp;
    return 0;
}

测试结果符合预期:
在这里插入图片描述

V3:单例模式(懒汉)线程池

我们再在V2的基础上,将线程池改为懒汉模式。

在此之前,我们来谈谈单例模式。


什么是单例模式

某些类,只应该具有一个对象(实例),就称之为单例。

正常情况下,一个类可以实例化出很多很多个对象,但对于某些场景来说,是不适合创建出多个对象的,比如本文的线程池,当程序运行后,仅需一个线程池对象来进行高效任务计算,因为多个线程池对象会大大增加调度成本,因此需要对 线程池类进行特殊设计,使其只能创建一个对象,换句话说就是不能让别人再创建对象。

总之,当一个对象可能会对计算机产生较大负担时,那么这个类就最好设计为单例模式。

在很多服务器开发场景中,经常需要让服务器加载很多的数据(上百G)到内存中,此时就往往要⼀个单例的类来管理这些数据。

在一个程序中只允许实例化出一个对象,可以通过单例模式来实现,单例模式是非常经典、常用、常考的设计模式。

什么是设计模式?
设计模式就是计算机大佬们在长时间项目实战中总结出来的解决方案,是帮助菜鸡编写高质量代码的利器,常见的设计模式有 单例模式、建造者模式、工厂模式、代理模式等

单例模式实现方法

实现一个单例模式的类非常简单,我们只需删除禁用一切可以实例化对象的默认成员函数构造、拷贝构造、赋值重载。然后在类的内部定义一个该类的对象作为静态成员即可。

单例模式有两种实现方向:饿汉 与 懒汉

[洗碗的例⼦]
吃完饭⽴刻洗碗,这种就是饿汉⽅式, 因为下⼀顿吃的时候可以⽴刻拿着碗就能吃饭.
吃完饭先把碗放下,然后下⼀顿饭⽤到这个碗了再洗碗,就是懒汉⽅式

饿汉方式最核⼼的思想是"预先加载",单例对象在程序编译时就加载好了

class Singleton 
{
 	 static Singleton* inst;
public:
	 static Singleton * GetInstance() 
	 {
	 	return inst;
	 }
};
// 静态成员类外初始化
Singleton Singleton::inst= new Singleton();
  • 优点:
    • 天生线程安全,实例在类加载时就创建完成,获取实例时不需要任何同步机制,也不存在多线程同时创建实例的竞争条件
    • 实现简单直观
    • 性能具有优势
  • 缺点:
    • 内存浪费(严重缺点):当对象比较大时,会非常浪费空间,从而影响性能。
    • 程序启动时间长:当对象比较大时,对象加载到内存会延长程序的启动时间

饿汉模式的缺点还是比较致命的,所以实践中一般不会优先考虑,除非场景恰到好处。

懒汉⽅式最核⼼的思想是"延时加载",当程序中使用该对象时才将该对象加载到内存。

实现懒汉的关键点在于定义一个静态的该类对象的指针作为成员,然后在获取单例的接口时在堆上new出对象,注意添加条件:只在第一次调用获取单例的接口时才new对象。否则就不是单例类了。

class Singleton 
{
 	 static Singleton* inst;
public:
	 static Singleton* GetInstance() 
	 {
		if (inst == nullptr) inst = new Singleton();
	 	return inst;
	 }
};

// 静态成员类外初始化
Singleton* Singleton::inst= nullptr;

然而,存在⼀个严重的问题,线程不安全。
当多线程并发去调用GetInstance时,由于线程切换的原因可能会有多个线程通过条件判断去创建对象,破坏单例。

因此,我们需要加互斥锁,来解决线程安全问题:

#include <pthread.h>
class Singleton 
{
 	 volatile static Singleton* inst;  // 需要设置 volatile 关键字, 否则可能被编译器优化. 
 	 static pthread_mutex_t lock; 
public:
	 static Singleton* GetInstance() 
	 {
		   if(inst == nullptr) // 双重判定空指针, 避免过多的线程前去申请锁, 降低锁冲突的概率, 提⾼性能
		   {
			 	 pthread_mutex_lock(&lock); // 使⽤互斥锁, 保证多线程情况下也只调⽤⼀次 new
				 if (inst == nullptr) 
				 {
				 		inst = new Singleton();
				 }
				 pthread_mutex_unlock(&lock);	
			}
		 	return inst;
	 }
};

// 静态成员类外初始化
volatile Singleton* Singleton::inst= nullptr;
pthread_mutex_t threadpool<T>::lock = PTHREAD_MUTEX_INITIALIZER; // 静态分配锁,无需初始化与释放

注意

  1. 加锁解锁的位置要在条件判断的花括号外,加锁在里,还是会创建多个对象,解锁在内,就可能解不了锁了。
  2. 双重判定空指针,避免过多的线程前去申请锁, 降低锁冲突的概率,提⾼性能。
  3. volatile关键字防止过度优化。编译器优化等级过高可能会发生new的指令重排。

new 操作进行指令正常的顺序应该是:
1. 分配内存
2. 调用构造函数初始化对象
3. 将地址赋值给 inst
但编译器/CPU 可能优化为:
1. 分配内存
2. 将地址赋值给 inst(此时对象还未构造!)
3. 调用构造函数初始化对象

// 线程A:正在创建单例
inst = new Singleton();  // 被重排后,先赋值地址,后构造

// 线程B:检查 inst
if (inst != nullptr) {  // 此时 inst 已不为空,但对象可能还未构造!
    inst->someMethod();  // 使用未完全构造的对象 → 灾难!
}

懒汉模式能够优化服务器的启动速度。
其他优缺点,几乎是与饿汉相对的:

  • 懒汉模式的优点:

    • 节省内存(按需加载)
    • 启动速度快
    • 灵活性高
    • 异常处理更好
    • 便于测试
  • 懒汉模式的缺点:

    • 线程安全问题(需要额外处理)
    • 性能开销(同步机制)
    • 实现复杂(正确的双重检查锁)
    • 首次调用延迟
    • 可能引发死锁

我们继续来实现线程池

我们选择实现懒汉模式,它更适合线程池。
有了上文的前置知识,现在你应该一看代码就懂了
代码如下
ThreadPoolV3.hpp:

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <pthread.h>
#include "Thread.hpp"
#include "task.hpp"
#include "block_queue.hpp"

#define NUM 10

template<class T>
class threadpool
{
private:
     std::string GetName(pthread_t tid)
     {
          for(auto &t : _threads)
          {
               if(t.GetTid() == tid)
                    return t.GetName();
          }
          return "None!";
     }

     // 处理任务(消费任务),线程池线程完成
     T Pop()
     {
          T t = _bq.pop();
          return t;
     }
public:
     // 实例化线程池的线程对象(注意此时线程还并未真正创建,需要start启动)
     void Init()
     {
          for(int i = 0; i < _num; ++i)
               _threads.push_back(Thread(i, HandlerTask, this));
     }

     // 线程的入口函数
     static void HandlerTask(void* args)
     {
          // 分离线程
          pthread_detach(pthread_self());

          threadpool<T> *tp = static_cast<threadpool<T> *>(args);
          std::string name = tp->GetName(pthread_self());
          while(true)
          {
               //消费任务
               Task t = tp->Pop();
               //处理任务
               t();
               std::cout << name << "处理任务:" << t.GetTask() << " 结果为:" << t.GetResult() << std::endl;
          }
     }

     void Start()//启动线程池,准备就绪
     {
          for(int i = 0; i < _num; ++i)
               _threads[i].Run();
     }

     // 添加任务(生产任务),主线程完成
     void Push(const T &task)
     {
          _bq.push(task);
     }
     
     static threadpool<T>* GetInstance(int num)
     {
        // 双重判断,避免过多的线程前去申请锁,提升效率
        if(_tp == nullptr)
        {
            //加锁,防止并发条件下可能new出多个对象
            pthread_mutex_lock(&_mutex);
            if(_tp == nullptr)
            {
               std::cout << "log: singleton create done first!" << std::endl;
               _tp = new threadpool<T>(num);
            }
            pthread_mutex_unlock(&_mutex);
        }
        return _tp;
     }
private:
     // 禁用一切可能创建多余对象的函数
    threadpool(int num = NUM)
     :_num(num)
    {}
    threadpool(const threadpool<T> &) = delete;
    const threadpool<T> &operator=(const threadpool<T> &) = delete;
    ~threadpool()
    {}
private:
     std::vector<Thread> _threads;       // 线程池
     int _num;                           // 线程池的线程数量
     block_queue<T> _bq;                 // 基于阻塞队列的生产者消费者模型
     volatile static threadpool<T> *_tp;        // 单例对象指针
     static pthread_mutex_t _mutex;      // 单例对象也属于共享资源,创建对象时需要加锁保护
};

// 静态成员变量需要类外初始化
template<class T>
volatile threadpool<T>* threadpool<T>::_tp = nullptr;

template<class T>
pthread_mutex_t threadpool<T>::_mutex = PTHREAD_MUTEX_INITIALIZER; // 静态分配锁,无需初始化与释放 

主线程代码:

#include <iostream>
#include <unistd.h>
#include <ctime>
#include "Thread.hpp"
#include "ThreadPoolV3.hpp"
#include "task.hpp"

using namespace std;

int main()
{
    threadpool<Task>::GetInstance(5)->Init();
    threadpool<Task>::GetInstance(5)->Start();
    srand(time(nullptr)/3); // 种随机数种子
    int len = opers.size();
    while(true)
    {
    		  // 生成计算任务
        int data1 = rand()%10 +1;
        usleep(10); // 隔开时间戳,避免前后随机数相近
        int data2 = rand()%10;
        char op = opers[rand()%len];
        Task t(data1, data2, op);
        // push任务到任务队列
		threadpool<Task>::GetInstance(5)->Push(t);
        cout << "main thread add a task:" << t.GetTask() << endl;
        sleep(1); // 模拟生成任务需要的时间
    }
    return 0;
}

测试结果符合预期:
在这里插入图片描述


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值