【Linux】线程池详解及其根本架构与单例模式实现

铁佛  论坛元老 | 2024-10-28 07:16:24 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1029|帖子 1029|积分 3087

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
目次
1.关于线程池的根本理论         
1.1.线程池是什么?
1.2.线程池的应用场景:
2.线程池的根本架构
2.1.线程容器
2.2.使命队列
2.3.线程函数(HandlerTask)
2.4.线程唤醒机制
3.添加单例模式
3.1.单例模式是什么?
3.2.饿汉实现方式和懒汉实现方式
饿汉式单例模式:
懒汉式单例模式:
3.3.改写懒汉式的单例模式
双判断的方式为什么能淘汰单例的加锁本钱呢?
单判断为什么会堕落?
单例模式的注意点:
4.代码和执行效果

1.关于线程池的根本理论         

1.1.线程池是什么?

一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和团体性能。而线程池维护着多个线程,等待着监视管理者分配可并发执行的使命。这避免了在处置惩罚短时间使命时创建与烧毁线程的代价。线程池不仅能够保证内核的充实利用,还能防止过分调度。可用线程数量应该取决于可用的并发处置惩罚器、处置惩罚器内核、内存、网络sockets等的数量。
1.2.线程池的应用场景:


  •  必要大量的线程来完成使命,且完成使命的时间比较短。 WEB服务器完成网页请求这样的使命,使用线程池技能是非常合适的。因为单个使命小,而使命数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的使命,好比一个Telnet毗连请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  •  对性能要求苛刻的应用,好比要求服务器迅速响应客户请求。
  • 担当突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池环境下,将产生大量线程,虽然理论上大部分操纵系统线程数量最大值不是问题,短时间内产生大量线程大概使内存到达极限,出现错误.
2.线程池的根本架构


  • 线程容器:用来管理创建的线程,方便统一初始化。
  • 使命队列:用来储存使命消息,必要支持压入与取出的操纵。
  • 线程函数(HandlerTask):线程都必要执行这个函数模块,在这个函数模块中举行使命的等待和执行。
  • 线程唤醒机制:必要一个线程唤醒机制,通过条件变量和互斥锁完成对线程的保护与唤醒。
  • 单例模式:线程池不必要创建多个,一个程序只必要一个线程池,通过单例模式举行优化。
2.1.线程容器

我们使用vector容器来存储线程,并且使用本身封装的线程来实现线程使用的各个接口
   std::vector<Thread> _threads;
  2.2.使命队列

我们使用队列这个容器来存储使命,并且利用队列FIFO的特性举行存储使命和取出使命
    std::queue<T> _task_queue;
  2.3.线程函数(HandlerTask)

我们起首要明确线程必要死循环去执行使命,以是必要while一直循环,直到线程池已经退出了&&使命队列是空的。执行使命的同时还必要保证线程的安全,以是必要加锁来保证。
  1.     void HandlerTask(std::string name)
  2.     {
  3.         LOG(INFO, "%s is running...", name.c_str());
  4.         //线程需要死循环去处理任务
  5.         while(true)
  6.         {
  7.             //1、保证队列安全
  8.             LockQueue();
  9.             //2、队列中不一定有数据
  10.             while(_task_queue.empty() && _isrunning)
  11.             {
  12.                 _waitnum++;
  13.                 ThreadSleep();
  14.                 _waitnum--;
  15.             }
  16.             //2.1 如果线程池已经退出了&&任务队列是空的
  17.             if(_task_queue.empty() && !_isrunning)
  18.             {
  19.                 UnlockQueue();
  20.                 break;
  21.             }
  22.             // 2.2 如果线程池不退出 && 任务队列不是空的
  23.             // 2.3 如果线程池已经退出 && 任务队列不是空的 --- 处理完所有的任务,然后在退出
  24.             // 3. 一定有任务, 处理任务
  25.             T t = _task_queue.front();
  26.             _task_queue.pop();
  27.             UnlockQueue();
  28.             LOG(DEBUG, "%s get a task", name.c_str());
  29.             //4.处理任务,这个任务属于线程独占的任务
  30.             //t();
  31.             LOG(DEBUG, "%s handler a task, result is: %s", name.c_str(), t.ResultToString().c_str());
  32.         }
  33.     }
复制代码
2.4.线程唤醒机制

必要一个线程唤醒机制,通过条件变量加互斥锁完成对线程的保护与唤醒。
3.添加单例模式

3.1.单例模式是什么?

某些类, 只应该具有一个对象(实例), 就称之为单例。在很多服务器开发场景中, 经常必要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据.
3.2.饿汉实现方式和懒汉实现方式

饿汉式单例模式:

饿汉式单例模式在类加载时就完成了实例的创建。这种方式的特点是线程安全,因为 JVM 在加载类时会对静态变量举行初始化,并且这个过程是线程互斥的。
  1. template <typename T>
  2. class Singleton {
  3. static T data;
  4. public:
  5. static T* GetInstance() {
  6. return &data;
  7. }
  8. };
复制代码
缺点:程序启动的时候,大概会很慢!以是我们一般不用饿汉
懒汉式单例模式:

  1. template <typename T>
  2. class Singleton {
  3. static T* inst;
  4. public:
  5. static T* GetInstance() {
  6. if (inst == NULL) {
  7. inst = new T();
  8. }
  9. return inst;
  10. }
  11. };
复制代码
缺点:存在一个严重的问题, 线程不安全.
第一次调用 GetInstance 的时候, 如果两个线程同时调用, 大概会创建出两份 T 对象的实例.
但是后续再次调用, 就没有问题了.
其着实一样平常使用中,我们一般不会使用饿汉式单例模式,因为它启动的时候过慢,以是我们来改写基于懒汉式的单例模式,重要办理线程安全的问题!
3.3.改写懒汉式的单例模式

添加双判断来办理线程安全问题。
  1.     static ThreadPool<T> *GetInstance()
  2.     {
  3.         // 如果是多线程获取线程池对象下面的代码就有问题了!!
  4.         // 只有第一次会创建对象,后续都是获取
  5.         // 双判断的方式,可以有效减少获取单例的加锁成本,而且保证线程安全
  6.         if (nullptr == _instance) // 保证第二次之后,所有线程,不用在加锁,直接返回_instance单例对象
  7.         {
  8.             LockGuard lockguard(&_lock);
  9.             if (nullptr == _instance)
  10.             {
  11.                 _instance = new ThreadPool<T>();
  12.                 _instance->InitThreadPool();
  13.                 _instance->Start();
  14.                 LOG(DEBUG, "创建线程池单例");
  15.                 return _instance;
  16.             }
  17.         }
  18.         LOG(DEBUG, "获取线程池单例");
  19.         return _instance;
  20.     }
复制代码
双判断的方式为什么能淘汰单例的加锁本钱呢?

我们重要办理的是畏惧多线程创建不止一个单例,我们的目标是让该单例模式只生产一个单例!围绕这一个核心去办理问题!
同时有很多进程过来的时候,都会去实验加锁,但是只有一个线程可以加锁成功,然后会执行new操纵,这时候_instance == nullptr就不建立了,再后来的线程不会等待在锁上了,直接判断外层的if就会退出了,不然所有的线程都要等待锁了。

单判断为什么会堕落?

同时大概多个线程通过if判断,等待锁,第一个线程加锁完成之后,执行创建,退出之后其他线程可以继承抢锁,抢到以后继承创建,就保证不了线程安全!
单例模式的注意点:



  • 单例模式下的构造函数必须要有,但必须是私有的。
  • 赋值和拷贝函数禁用,因为只创建1个单例
  • 在类内里创建的静态变量在类内定义,必要在类外初始化
4.代码和执行效果

代码:
  1. #pragma once#include <iostream>#include <vector>#include <queue>#include <pthread.h>#include "Log.hpp"#include "Thread.hpp"#include "LockGuard.hpp"using namespace ThreadModule;const static int gdefaultthreadnum = 5;template <typename T>class ThreadPool{public:    static ThreadPool<T> *GetInstance()
  2.     {
  3.         // 如果是多线程获取线程池对象下面的代码就有问题了!!
  4.         // 只有第一次会创建对象,后续都是获取
  5.         // 双判断的方式,可以有效减少获取单例的加锁成本,而且保证线程安全
  6.         if (nullptr == _instance) // 保证第二次之后,所有线程,不用在加锁,直接返回_instance单例对象
  7.         {
  8.             LockGuard lockguard(&_lock);
  9.             if (nullptr == _instance)
  10.             {
  11.                 _instance = new ThreadPool<T>();
  12.                 _instance->InitThreadPool();
  13.                 _instance->Start();
  14.                 LOG(DEBUG, "创建线程池单例");
  15.                 return _instance;
  16.             }
  17.         }
  18.         LOG(DEBUG, "获取线程池单例");
  19.         return _instance;
  20.     }    void Stop()    {        LockQueue();        _isrunning = false;        ThreadWakeup();        UnlockQueue();    }    void Wait()    {        for(auto &thread : _threads)        {            thread.Join();            LOG(INFO, "%s is quit...", thread.name().c_str());        }    }    bool Enqueue(const T &t)    {        bool ret = false;        LockQueue();        if(_isrunning)        {            _task_queue.push(t);            if(_waitnum > 0)            {                ThreadWakeup();            }            LOG(DEBUG, "enqueue task success");            ret = true;        }        UnlockQueue();        return ret;    }    ~ThreadPool()    {        pthread_mutex_destroy(&_mutex);        pthread_cond_destroy(&_cond);    }private:    void LockQueue()    {        pthread_mutex_lock(&_mutex);    }    void UnlockQueue()    {        pthread_mutex_unlock(&_mutex);    }    void ThreadSleep()    {        pthread_mutex_unlock(&_mutex);    }    void ThreadWakeup()    {        // 唤醒一个等待特定条件变量的线程        pthread_cond_signal(&_cond);    }    void ThreadWakeupAll()    {        // 唤醒所有等待特定条件变量的线程        pthread_cond_broadcast(&_cond);    }    // 单例模式下的构造函数必须要有,但必须是私有的    ThreadPool(int threadnum = gdefaultthreadnum) : _threadnum(threadnum), _waitnum(0), _isrunning(false)    {        pthread_mutex_init(&_mutex, nullptr);        pthread_cond_init(&_cond, nullptr);        LOG(INFO, "ThreadPool Construct()");    }    // 赋值和拷贝函数禁用,因为只创建1个单例    ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;    ThreadPool(const ThreadPool<T> &) = delete;    void Start()    {        for (auto &thread : _threads)        {            thread.Start();        }    }    void InitThreadPool()    {        //构建出所有的线程,并不启动        for(int num = 0; num < _threadnum; num++)        {            std::string name = "thread" + std::to_string(num+1);            //bind函数到底有什么作用???            _threads.emplace_back(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1), name);            LOG(INFO, "init thread %s done", name.c_str());        }    }    void HandlerTask(std::string name)
  21.     {
  22.         LOG(INFO, "%s is running...", name.c_str());
  23.         //线程需要死循环去处理任务
  24.         while(true)
  25.         {
  26.             //1、保证队列安全
  27.             LockQueue();
  28.             //2、队列中不一定有数据
  29.             while(_task_queue.empty() && _isrunning)
  30.             {
  31.                 _waitnum++;
  32.                 ThreadSleep();
  33.                 _waitnum--;
  34.             }
  35.             //2.1 如果线程池已经退出了&&任务队列是空的
  36.             if(_task_queue.empty() && !_isrunning)
  37.             {
  38.                 UnlockQueue();
  39.                 break;
  40.             }
  41.             // 2.2 如果线程池不退出 && 任务队列不是空的
  42.             // 2.3 如果线程池已经退出 && 任务队列不是空的 --- 处理完所有的任务,然后在退出
  43.             // 3. 一定有任务, 处理任务
  44.             T t = _task_queue.front();
  45.             _task_queue.pop();
  46.             UnlockQueue();
  47.             LOG(DEBUG, "%s get a task", name.c_str());
  48.             //4.处理任务,这个任务属于线程独占的任务
  49.             //t();
  50.             LOG(DEBUG, "%s handler a task, result is: %s", name.c_str(), t.ResultToString().c_str());
  51.         }
  52.     }
  53.     int _threadnum;    std::vector<Thread> _threads;    std::queue<T> _task_queue;    pthread_mutex_t _mutex;    pthread_cond_t _cond;    int _waitnum;    bool _isrunning;    // 添加单例模式    static ThreadPool<T> *_instance;    static pthread_mutex_t _lock;};// 在类内里创建的静态变量在类内定义,必要在类外初始化template <typename T>ThreadPool<T> *ThreadPool<T>::_instance = nullptr;template <typename T>pthread_mutex_t ThreadPool<T>::_lock = PTHREAD_MUTEX_INITIALIZER;// 为什么双重判断就可以办理线程安全的问题?// 为什么static就可以不用创建对象直接调用函数呢?
复制代码
执行效果:


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

铁佛

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表