自学内容网 自学内容网

线程池简易版

目录

一、什么是线程池

二、线程池的实现

三、单例模式

(1)饿汉模式

(2)懒汉模式

四、在线程池的基础上修改成懒汉式单例模式


一、什么是线程池

        线程池是一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价(一直都是那么几个线程在执行任务,这些线程永远不销毁,一直处于等待任务,处理任务的状态)。线程池不仅能够保证内核的充分利
用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的应用场景:

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

线程池最关键的两个步骤:
(1)创建固定数量线程池,循环从任务队列中获取任务对象,
(2)获取到任务对象后,执行任务对象中的任务接口

二、线程池的实现

Thread_Pool.hpp

#pragma once

#include <iostream>
#include <queue>
#include <vector>

#include <functional>
#include <thread>
#include <mutex>
#include <condition_variable>

class func_data
{
public:
    func_data(int i)
    {
        std::string name = "thread-";
        name += std::to_string(i);
        _thread_name = name;
    }

    std::string get_func_data()
    {
        return _thread_name;
    }
    void Print_func_data()
    {
        std::cout << _thread_name << std::endl;
    }

private:
    std::string _thread_name;
};

// 将函数类型定义为func_t
using func_t = std::function<void(func_data)>;

class Thread_Pool
{
private:
    // 构造
    Thread_Pool(size_t thread_num = 5);
    // 拷贝构造
    Thread_Pool(const Thread_Pool &tp) = delete;
    // 赋值运算符重载
    const Thread_Pool &operator=(const Thread_Pool &tp) = delete;

public:

    //获取单例模式的线程池指针
    static Thread_Pool *Get_instance();

    // 析构
    ~Thread_Pool();
    // 主线程等待回收子线程
    void Wait();
    // 主线程插入任务
    void Push_task(func_t f);
    // 其他线程拿任务
    void Get_task(func_data d);

private:
    std::queue<func_t> _tasks;         // 任务队列
    std::vector<std::thread> _workers; // 线程

    size_t _threadnum; // 线程数量

    std::mutex _mutex;             // 取任务时保证线程安全的锁
    std::condition_variable _cond; // 有没有任务的条件变量

    static Thread_Pool* instance;   //单例模式的线程池指针
};


//获取单例模式的线程池指针
Thread_Pool *Thread_Pool::Get_instance()
{
    if (instance == nullptr)
    {
        instance = new Thread_Pool();
        std::cout<<"创建单例线程池成功"<<std::endl;
    }
    return instance;
}

// 其他线程拿任务
void Thread_Pool::Get_task(func_data d)
{
    // 要while(1)的原因是,其他线程要不停的从任务队列中拿任务
    while (1)
    {
        // 定义一个可执行对象
        func_t fun;
        {
            // 访问临界区先要线程安全
            std::unique_lock<std::mutex> lock(_mutex);
            // 拿任务(先要判断有没有任务,没有任务阻塞在条件变量中)
            while (_tasks.empty())
            {
                _cond.wait(lock); // 只能传入一把unique_lock的锁
            }
            fun = _tasks.front();
            _tasks.pop();
        }
        //可执行对象fun需要一个func_data的参数,这个我们已经传进来了
        fun(d);
    }
}

// 主线程插入任务
void Thread_Pool::Push_task(func_t in)
{
    // 访问临界区先要线程安全
    std::unique_lock<std::mutex> lock(_mutex);
    // 向队列中插入任务
    _tasks.push(in);
    // 插入完成唤醒一个线程
    _cond.notify_one();
}

// 主线程等待回收子线程
void Thread_Pool::Wait()
{
    for (auto &t : _workers)
    {
        t.join();
    }
}

// 析构
Thread_Pool::~Thread_Pool()
{
    std::cout << "线程池销毁" << std::endl;
}

// 构造
Thread_Pool::Thread_Pool(size_t thread_num)
    : _threadnum(thread_num)
{
    // 创建n个线程
    for (int i = 0; i < _threadnum; i++)
    {
        // c++线程库中的构造,第一个参数是可执行对象,后面的都是该可执行对象的参数
        // 构造的时候就把任务传进去
        // std::thread t(std::bind(&Thread_Pool::Thread_run, this, std::placeholders::_1), func_data(i));
        // std::thread t([](func_data d){d.Print_func_data();}, func_data(i));

        // 但是我们不想让任务是写死的,想让他从任务队列中拿,我们想用Get_task来获取任务,
        // 但是Get_task是一个成员函数,里面有this指针,我们用bind来规避指针(也可以使用静态成员来规避this指针)
        // bind的作用是将一个函数绑定,至于新产生的可执行对象有几个参数是看placeholder有几个
        // 而thread的第一个参数是可执行对象,第二个参数是该可执行对象的形参

        //fd是线程执行函数的参数结构体
        func_data fd = func_data(i);
        std::thread t(std::bind(&Thread_Pool::Get_task, this, std::placeholders::_1), fd);
        // 这里的func_data是一个临时对象,但是我又不想在线程池中用一个vector<func_data来存储数据,所有我被迫把Get_task改成了非引用的样子,幸好我的func_data不大,否则拷贝效率会很低

        std::cout << "main线程创建了" << fd.get_func_data() << std::endl;

        // 插入到线程池中
        _workers.push_back(move(t)); // 这里会发生拷贝构造,但是thread的拷贝构造是被禁用的,所以我们可以使用移动语义
    }
}


// 创建线程池单例指针
Thread_Pool* Thread_Pool:: instance=nullptr;

main.cc

#include"Thread_Pool.hpp"
#include<unistd.h>



void f(func_data)
{
    std::cout<<gettid()<<"正在执行"<<std::endl;
}

int main()
{
    // //创建一个线程池对象,用智能指针去管理
    // std::unique_ptr<Thread_Pool> tp(new Thread_Pool(5));

    Thread_Pool*tp=Thread_Pool::Get_instance();

    //在创建线程池的时候就会去调用Get_task,但是因为没有任务,所以5个线程都阻塞在了_cond条件变量中
    
    //主线程插入任务
    while(1)
    {
        Thread_Pool::Get_instance()->Push_task(f);
        // tp->Push_task(f);
        sleep(1);
    }

    Thread_Pool::Get_instance()->Wait();
    // tp->Wait();

    return 0;
}

        以后我们想要给线程池插入不同的任务,只需要写一个函数用来生产void(func_data)即可,这里的func_data也可以自定义,在Thread_Pool.hpp中修改一下即可。

相关说明:

        我们创建好了线程池之后,首次我们先是对其进行初始化操作;然后不断的向任务队列塞数据,由线程池中的线程去获取任务并执行相关操作。

        1.任务队列(即临界资源)是会被多个执行流同时访问,因此我们需要引入互斥锁对任务队列进行保护。

        2.线程池中的线程想要获取到任务队列中的任务,那么就必须要确保任务队列中有任务,所以我们还需引入条件变量来进行判断,如果队列中没有任务,线程池中的线程将会被挂起,直到任务队列中有任务后才被唤醒;

        3.在thread_pool.hpp中,多线程去执行对应的方法的时候,采用的是成员函数+bind函数,这样做的目的是解决类中存在隐藏的this指针问题,因为多线程在调用对应的函数时,该函数只有一个形参,不用bind将this指针绑定的话,形参就需要两个参数,是不可以的;所以我们可以将this指针作为参数绑定好,就可以访问类内的成员函数了。

        总结:在main创建线程池的时候,需要创建几个线程,而创建线程的时候需要传入可执行对象,这里我们传递的是Thread_Pool的类内方法,而这个方法,执行的是不断从任务队列中拿任务,然后执行。bind之后其实就相当于在类外定义了了一个方法。

三、单例模式

         单例(Singleton)模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例 ;

使用场景:

1.语义上只需要一个
2.该对象内部存在大量的空间,保存了大量的数据,如果允许该对象存在多份,或者允许发生各种拷贝,内存中存在冗余数据;
一般Singleton模式通常有三种形式:

        饿汉式:吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭。
        懒汉式:吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式。
        懒汉方式最核心的思想是 "延时加载"。(例如我们之前所学过的写时拷贝)从而能够优化服务器的启动速度。

(1)饿汉模式

(2)懒汉模式

四、在线程池的基础上修改成懒汉式单例模式

最后在类外定义静态成员


原文地址:https://blog.csdn.net/2303_79336820/article/details/144265007

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!