线程池及其代码

        线程池是一种线程使用模式,用来管理和维护多个线程,避免在短时间内创建和销毁线程的代价,不仅能够保证内核的充分利用,而且能够防止过分调度

 线程池的应用场景

  1. 需要大量线程完成任务,且完成的时间比较短。
  2. 对性能要求苛刻的应用。
  3. 接收突发性的大量请求,但不至于使服务器因此产生大量线程的应用。

         接下来我们直接看看源码;


#include "RingQueue.hpp"
#include 
#include 
#include 
#include 

#include 

using namespace std;
static int maxcap = 10;
static int num = 1;

template 
class ThreadPool;
template 
class PoolData;

class Thread
{
    typedef function func_t;

public:
    Thread()
    {
        char buffer[1024];
        snprintf(buffer, sizeof(buffer), "Thread->%d", num++);
        _name = static_cast(buffer);
    }

    void start(func_t func, void *args) //_args 是一个指针,内部包含着线程池的指针
    {
        _func = func;
        _args = args;
        int n = pthread_create(&_pid, NULL, start_routine, this); // 由于create函数中没有this指针,因此不能直接使用_func

        assert(n == 0);
        (void)n;
    }

    static void *start_routine(void *args) // create中的函数不能有this指针,但是它可以用作中转站调用我们想调用的函数
    {
        Thread *t = static_cast(args);

        t->run(t->_args);
    }

    void *run(void *args)
    {

        this->_func(args); // 这个_func实际上就是我们线程池中的_handler函数
    }

    pthread_t _pid;
    string _name;
    func_t _func;
    void *_args;
};

template 
class PoolData
{
public:
    PoolData(ThreadPool *pool)
        : _pool(pool)
    {
    }

public:
    ThreadPool *_pool;
};

template 
class ThreadPool
{

public:
    ThreadPool(int cap = maxcap)
        : _cap(cap)
    {
        _q = new RingQueue();
        pthread_cond_init(&_cond, NULL);
        pthread_mutex_init(&_mutex, NULL);
    }
    // 初始化线程池
    void init()
    {
        for (int i = 0; i < _cap; i++)
        {
            Thread *t = new Thread();

            _v.push_back(t);
        }
        for (int i = 0; i < _cap; i++)
        {
            cout << _v[i]->_name << " is read..." << endl;
        }
    }

    static void *_handler(void *args)
    {
        PoolData *pd = static_cast *>(args);
        while (true)
        {
            T t;

            pthread_mutex_lock(&pd->_pool->_mutex);

            if (pd->_pool->is_empty())
            {

                pthread_cond_wait(&pd->_pool->_cond, &pd->_pool->_mutex);
            }
            pd->_pool->Pop(&t);
            cout << "任务已获取 -> " << t << endl;
            sleep(1);
            pthread_mutex_unlock(&pd->_pool->_mutex);
        }
    }

    bool is_empty()
    {
        return _q->is_empty();
    }

    // 线程池运行
    void run()
    {
        for (auto &t : _v)
        {
            PoolData *pd = new PoolData(this);
            t->start(_handler, pd);
            cout << t->_name << "start..." << endl;
        }
    }

    // 放入任务
    void Push(const T &t)
    {
        // pthread_mutex_lock(&_mutex);
        // _q->push(t);

        // pthread_cond_signal(&_cond);

        // pthread_mutex_unlock(&_mutex);
        _q->Push(t);
    }
    // 弹出结果
    void Pop(T *t)
    {
        // *t = _q->front();
        // _q->pop();
        _q->Pop(t);
    }

private:
    RingQueue* _q;//放任务
    // queue *_q;
    vector _v; // 放线程
    pthread_cond_t _cond;
    pthread_mutex_t _mutex;
    int _cap; // 最大线程容量
};

        为了实现线程和线程池之间的解耦,这里采用了两个类分别保存线程和线程池。

        在Thread类中我们保存了线程所需要运行的函数,名字等必要的信息,而线程池内有保存任务和数据的环形队列,保存线程的数组,和保证互斥同步功能的互斥锁和条件变量。

        接着来将一些小细节。

初始化线程池

线程池及其代码_第1张图片

         在初始化线程池中,我们根据设定的最大变量来new出来我们的新线程,并且放入数组中。

线程池运行

线程池及其代码_第2张图片

         线程池的运行是利用一个循环,并且用一个类对象保存线程池的this指针,然后再调用Thread类中的start函数,将线程池中的_hadler函数和保存线程池this指针的对象传递过去。

        为何我们传递过去的函数是线程池中的_handler函数而不是线程原本需要运行的函数呢?

        这是因为我们线程类中没有保存数据或任务的队列只能在线程池中完成任务

        而传递线程池的this指针的原因后续再说明。

线程类的start函数

线程池及其代码_第3张图片

         start函数的参数是需要运行的函数的指针以及保存了线程池的this指针的指针。

        我们把形参赋给类内成员后,便调用我们的create函数。

        但是有一点就是,create函数的函数指针不能有this指针,也就是说类内的成员函数必须是static的,否则无法成功调用create函数,因此我们在Thread类内将start_routine函数设为static函数,由于static函数没有this指针, 因此传过去的变量必须是this指针

start_routine函数

线程池及其代码_第4张图片

         在start_routine函数中,我们调用static_cast来进行强转接收this指针,并且用一个Thread类指针对象接收,然后调用类对象的_func函数,并且将t->_args传递过去,这样就能回到线程池中完成回调。

_handler函数

线程池及其代码_第5张图片

         该函数必须是static的,否则在Thread函数中调用_func时会出现报错。

        而由于static函数没有this指针,但我们要是想访问临界资源就必须有ThreadPool的this指针,这就是为什么前面的run函数我们一定要传PoolData类对象的指针的原因,然后便是取出任务后完成任务。

        不过需要注意的是,我这里的 t 并不是任务,这里我默认使用的是模版类型是 int 类型,若是存储的任务,此处应该按照任务的类或者方式来完成任务。

测试

  

#include"ThreadPool.hpp"
#include
#include
using namespace std;

int main()
{
    srand((unsigned int)time(NULL));
    ThreadPool* tp = new ThreadPool();
    tp->init();
    tp->run();
    while(true)
    {   
        int data = rand()%100;
        cout<<"data : "<Push(data);
        sleep(1);
    }
    return 0;
}

        此处我使用的是int类型,并且每次出现一个随机数就输出并放入线程池中,我们看看结果。

线程池及其代码_第6张图片 

         能看到结果正确。

单例模式的线程池

饿汉模式

        饿汉模式是一开始就创建,不需要申请,我们看看饿汉模式下的线程池吧。

        

线程池及其代码_第7张图片

         首先最重要的就是在线程池内部创建一个static类型的变量,然后在类外初始化。

线程池及其代码_第8张图片

         其次由于是单例,只能有一个对象,那么就需要将构造函数,拷贝构造,赋值重载之类的函数全部私有化。

        

线程池及其代码_第9张图片

         最后设一个函数,用来给外面获取该单例对象。

        便大功告成了。

#include"ThreadPool.hpp"
#include
#include
using namespace std;

int main()
{
    srand((unsigned int)time(NULL));
    ThreadPool* tp = ThreadPool::getInstance();
    tp->init();
    tp->run();
    while(true)
    {   
        int data = rand()%100;
        cout<<"data : "<Push(data); 
        sleep(1);
    }
    return 0;
}

线程池及其代码_第10张图片

 懒汉模式

        懒汉模式是使用时才申请,我们来看看吧。

线程池及其代码_第11张图片

         既然是申请时再使用,那么就一定是一个指针,用来动态开辟出来的。

        而动态开辟就会涉及到线程安全问题,没有锁时外部调用getInstance函数可能会new两个函数。于是就需要一个锁。

        

线程池及其代码_第12张图片

         并且它的getInstance需要两次判空,防止出现重复new的情况。

        其他的就和饿汉模式一样了,看看结果。

线程池及其代码_第13张图片

        发现结果正确。 

你可能感兴趣的:(Linux,开发语言,linux,c++)