• Linux —— 线程池


    目录

    一、什么是线程池

    二、线程池的优点

    三、线程池的应用

    四、实现一个简单的线程池

    五、单例模式

    1. 饿汉实现方式

    2. 懒汉实现方式

    3. 单例模式实现线程池(懒汉方式)

    六、其他常见的各种锁


    一、什么是线程池

            线程池是线程的一种使用模式。在前面的情况中,我们都是遇到任务然后创建线程再执行。但是线程的频繁创建就类似于内存的频繁申请,会给操作系统带来更大的压力,进而影响整体的性能。

            所以我们一次申请好一定数量而定线程,然后将线程的管理操作交给线程池,就避免了在短时间内不断创建与销毁线程的代价,线程池不但能够保证内核的充分利用,还能防止过分调度,并根据实际业务情况进行修改。 

    二、线程池的优点

    1. 任务来到立马就有线程去执行任务,节省了创建线程的时间。
    2. 防止服务器线程过多导致的系统过载问题
    3. 相对于进程池,线程池资源占用较少,但是健壮性很差 

    三、线程池的应用

    需要大量的线程来完成任务,且完成任务的时间比较短 

    • 例如:WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。

    对性能要求苛刻的应用

    • 比如要求服务器迅速响应客户请求。

    接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用

    • 突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

    四、实现一个简单的线程池

    线程池中提供了一个任务队列,以及若干个线程。示意图如下:

    thread_pool.hpp

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. namespace ns_threadpool
    9. {
    10. const int g_num = 5;
    11. template <class T>
    12. class ThreadPool
    13. {
    14. private:
    15. int num_; //固定大小的线程池
    16. queue task_queue_; //任务队列,使用STL的queue实现
    17. pthread_mutex_t mtx_; //定义一把锁
    18. pthread_cond_t cond_; //定义一个条件变量
    19. public:
    20. void Lock() { pthread_mutex_lock(&mtx_);} //加锁操作
    21. void Unlock() { pthread_mutex_unlock(&mtx_);} //解锁操作
    22. bool IsEmpety() { return task_queue_.empty();} //判断任务队列是否为空
    23. void Wait() { pthread_cond_wait(&cond_, &mtx_);} //让线程在条件变量下等待
    24. void WakeUp() { pthread_cond_signal(&cond_);} //唤醒在条件变量下等待的线程
    25. public:
    26. ThreadPool(int num = g_num):num_(num)
    27. {
    28. pthread_mutex_init(&mtx_, nullptr);
    29. pthread_cond_init(&cond_, nullptr);
    30. }
    31. //在类中要让线程执行类内成员方法,是不可行的
    32. //必须让线程执行静态方法
    33. static void* Rountine(void* args)
    34. {
    35. pthread_detach(pthread_self());
    36. ThreadPool* tp = (ThreadPool*)args;
    37. while(true)
    38. {
    39. tp->Lock();
    40. while(tp->IsEmpety())
    41. {
    42. tp->Wait();
    43. }
    44. T t;
    45. tp->PopTask(&t);
    46. tp->Unlock();
    47. t.Run();
    48. }
    49. }
    50. void InitThreadPool()
    51. {
    52. pthread_t tid;
    53. for(int i = 0; i < num_; i++)
    54. {
    55. pthread_create(&tid, nullptr, Rountine, (void*)this);
    56. }
    57. }
    58. void PushTask(const T& in)//向任务队列添加任务
    59. {
    60. Lock();
    61. task_queue_.push(in);
    62. Unlock();
    63. WakeUp();
    64. }
    65. void PopTask(T* out)//从任务队列获取任务
    66. {
    67. *out = task_queue_.front();
    68. task_queue_.pop();
    69. }
    70. ~ThreadPool()
    71. {
    72. pthread_mutex_destroy(&mtx_);
    73. pthread_cond_destroy(&cond_);
    74. }
    75. };
    76. }

    Task.hpp

    1. #pragma once
    2. #include
    3. #include
    4. using namespace std;
    5. namespace ns_task
    6. {
    7. class Task
    8. {
    9. private:
    10. int x_;
    11. int y_;
    12. char op_;//用来表示:+ 、- 、* 、/ 、%
    13. public:
    14. Task(){}
    15. Task(int x, int y, char op):x_(x), y_(y), op_(op){}
    16. string show()
    17. {
    18. string message = to_string(x_);
    19. message += op_;
    20. message += to_string(y_);
    21. message += "=?";
    22. return message;
    23. }
    24. int Run()
    25. {
    26. int res = 0;
    27. switch(op_)
    28. {
    29. case '+':
    30. res = x_ + y_;
    31. break;
    32. case '-':
    33. res = x_ - y_;
    34. break;
    35. case '*':
    36. res = x_ * y_;
    37. break;
    38. case '/':
    39. res = x_ / y_;
    40. break;
    41. case '%':
    42. res = x_ % y_;
    43. break;
    44. default:
    45. cout << "bug" << endl;
    46. break;
    47. }
    48. printf("当前任务正在被:%lu处理,处理结果为:%d %c %d = %d\n",pthread_self(), x_, op_, y_, res);
    49. return res;
    50. }
    51. int operator()()
    52. {
    53. return Run();
    54. }
    55. ~Task(){}
    56. };
    57. }

    main.cc

    1. #include "thread_pool.hpp"
    2. #include "Task.hpp"
    3. #include
    4. #include
    5. using namespace ns_threadpool;
    6. using namespace ns_task;
    7. int main()
    8. {
    9. ThreadPool* tp = new ThreadPool();//创建线程池
    10. tp->InitThreadPool(); //进行初始化
    11. srand((long long)time(nullptr));//生产随机数
    12. while(true) //不断向任务队列塞数据
    13. {
    14. Task t(rand() % 20 + 1, rand() % 10 + 1, "+-*/%"[rand() % 5]);
    15. tp->PushTask(t);
    16. sleep(1);
    17. }
    18. return 0;
    19. }

    相关说明:

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

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

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

            3.在thread_pool.hpp中,多线程去执行对应的方法的时候,采用的是静态成员函数,这样做的目的是解决类中存在隐藏的this指针问题,因为多线程在调用对应的函数时,该函数只有一个形参,不加static的话,那么形参个数就有两个,是不可以的;所以我们可以将this指针作为参数传递过去,就可以访问类内的成员函数了;

    运行代码后一瞬间就有六个线程,其中一个是主线程,另外五个是线程池内处理任务的线程。

    五、单例模式

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

    使用场景:

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

    一般Singleton模式通常有三种形式:

    • 饿汉式:吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭。
    • 懒汉式:吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式。

            懒汉方式最核心的思想是 "延时加载"。(例如我们之前所学过的写时拷贝)从而能够优化服务器的启动速度。

    1. 饿汉实现方式

    该模式在类被加载时就会实例化一个对象,具体代码如下:

    1. template <typename T>
    2. class Singleton
    3. {
    4. private:
    5. static Singleton data;//饿汉模式,在加载的时候对象就已经存在了
    6. public:
    7. static Singleton* GetInstance()
    8. {
    9. return &data;
    10. }
    11. };

            该模式能简单快速的创建一个单例对象,而且是线程安全的(只在类加载时才会初始化,以后都不会)。但它有一个缺点,就是不管你要不要都会直接创建一个对象,会消耗一定的性能(当然很小很小,几乎可以忽略不计,所以这种模式在很多场合十分常用而且十分简单) 

    2. 懒汉实现方式

    该模式只在你需要对象时才会生成单例对象(比如调用GetInstance方法) 

    1. template <typename T>
    2. class Singleton
    3. {
    4. private:
    5. static Singleton* inst; //懒汉式单例,只有在调用GetInstance时才会实例化一个单例对象
    6. public:
    7. static Singleton* GetInstance()
    8. {
    9. if (inst == NULL)
    10. {
    11. inst = new Singleton();
    12. }
    13. return inst;
    14. }
    15. };

            看上去,这段代码没什么明显问题,但它不是线程安全的。假设当前有多个线程同时调用GetInstance()方法,由于当前还没有对象生成,那么就会由多个线程创建多个对象。

    1. // 懒汉模式, 线程安全
    2. template <typename T>
    3. class Singleton
    4. {
    5. private:
    6. static Singleton* inst;
    7. static std::mutex lock;
    8. public:
    9. static T* GetInstance()
    10. {
    11. if (inst == NULL) // 双重判定空指针, 降低锁冲突的概率, 提高性能
    12. {
    13. lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new
    14. if (inst == NULL)
    15. {
    16. inst = new T();
    17. }
    18. lock.unlock();
    19. }
    20. return inst;
    21. }
    22. };

            这种形式是在懒汉方式的基础上增加的,当多个线程调用GetInstance方法时,此时类中没有对象,那么多个线程就会来到锁的位置,竞争锁。必然只能有一个线程竞争锁成功,此时再次判断有没有对象被创建(就是inst指针),如果没有就会new一个对象,如果有就会解锁,并返回已有的对象;

            总的来说,这样的形式使得多个线程调用GetInstance方法时,无论成功与否,都会有返回值;

    3. 单例模式实现线程池(懒汉方式)

    thread_pool.hpp 

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. namespace ns_threadpool
    9. {
    10. const int g_num = 5;
    11. template <class T>
    12. class ThreadPool
    13. {
    14. private:
    15. int num_;
    16. queue task_queue_;
    17. pthread_mutex_t mtx_;
    18. pthread_cond_t cond_;
    19. static ThreadPool* ins;//类内的静态指针
    20. private:
    21. //构造函数必须得实现,必须初始化
    22. ThreadPool(int num = g_num):num_(num)
    23. {
    24. pthread_mutex_init(&mtx_, nullptr);
    25. pthread_cond_init(&cond_, nullptr);
    26. }
    27. ThreadPool(const ThreadPool& tp) = delete;
    28. ThreadPool& operator=(ThreadPool& tp) = delete;
    29. public:
    30. static ThreadPool* GetInstance()
    31. {
    32. //使用静态的锁是不需要初始化和销毁的
    33. static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    34. if(ins == nullptr)//双判定,减少锁的征用,提高获取单例的效率
    35. {
    36. pthread_mutex_lock(&lock);
    37. if(ins == nullptr)
    38. {
    39. ins = new ThreadPool();
    40. ins->InitThreadPool();
    41. cout << "首次加载对象..." << endl;
    42. }
    43. pthread_mutex_unlock(&lock);
    44. }
    45. return ins;
    46. }
    47. void Lock() { pthread_mutex_lock(&mtx_);}
    48. void Unlock() { pthread_mutex_unlock(&mtx_);}
    49. bool IsEmpety(){ return task_queue_.empty();}
    50. void Wait() { pthread_cond_wait(&cond_, &mtx_);}
    51. void WakeUp() { pthread_cond_signal(&cond_);}
    52. public:
    53. //在类中要让线程执行类内成员方法,是不可行的
    54. //必须让线程执行静态方法
    55. static void* Rountine(void* args)
    56. {
    57. pthread_detach(pthread_self());
    58. ThreadPool* tp = (ThreadPool*)args;
    59. while(true)
    60. {
    61. tp->Lock();
    62. while(tp->IsEmpety())
    63. {
    64. tp->Wait();
    65. }
    66. T t;
    67. tp->PopTask(&t);
    68. tp->Unlock();
    69. t.Run();
    70. }
    71. }
    72. void InitThreadPool()
    73. {
    74. pthread_t tid;
    75. for(int i = 0; i < num_; i++)
    76. {
    77. pthread_create(&tid, nullptr, Rountine, (void*)this);
    78. }
    79. }
    80. void PushTask(const T& in)
    81. {
    82. Lock();
    83. task_queue_.push(in);
    84. Unlock();
    85. WakeUp();
    86. }
    87. void PopTask(T* out)
    88. {
    89. *out = task_queue_.front();
    90. task_queue_.pop();
    91. }
    92. ~ThreadPool()
    93. {
    94. pthread_mutex_destroy(&mtx_);
    95. pthread_cond_destroy(&cond_);
    96. }
    97. };
    98. template<class T>
    99. ThreadPool* ThreadPool::ins = nullptr;
    100. }

    main.cc

    1. #include "thread_pool.hpp"
    2. #include "Task.hpp"
    3. #include
    4. #include
    5. using namespace ns_threadpool;
    6. using namespace ns_task;
    7. int main()
    8. {
    9. cout << "当前正在运行我的进程和其他代码......" << endl;
    10. cout << "当前正在运行我的进程和其他代码......" << endl;
    11. cout << "当前正在运行我的进程和其他代码......" << endl;
    12. cout << "当前正在运行我的进程和其他代码......" << endl;
    13. cout << "当前正在运行我的进程和其他代码......" << endl;
    14. cout << "当前正在运行我的进程和其他代码......" << endl;
    15. cout << "当前正在运行我的进程和其他代码......" << endl;
    16. sleep(5);
    17. srand((long long)time(nullptr));
    18. while(true)
    19. {
    20. sleep(1);
    21. Task t(rand() % 20 + 1, rand() % 10 + 1, "+-*/%"[rand() % 5]);
    22. ThreadPool::GetInstance()->PushTask(t);
    23. //单列本身会在任何场景下,任何环境下被调用
    24. //GetInstans():被多线程重入,进而导致线程安全的问题
    25. cout << ThreadPool::GetInstance() << endl;//获取对象的地址
    26. }
    27. return 0;
    28. }

    Task.hpp同上

    运行后发现对象的地址是一样的,表明单例调用成功了,只存在一份;

    六、其他常见的各种锁

    悲观锁:
            在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
             悲观锁适用于写多读少的情况下,即:需要频繁的写数据时候,可以考虑使用悲观锁。
    乐观锁:
            每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。
             乐观锁适用于读多写少的情况下,即:读数据多余写数据的时候,可以考虑使用乐观锁。
    自旋锁:
             当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。  
  • 相关阅读:
    oracle---一表向另一表循环插入数据过程中、发现异常并抛进日志表、直至数据传输完成
    js Ajax函数封装及使用
    Python文件及目录操作(高级文件操作篇)
    Java — static修饰符
    JMM对数据竞争的定义
    ASPX与ASP URL传递值问题
    Redis Lua脚本实现分布式锁
    uniapp 跳转页面保存和刷新 拦截器的使用
    计算机毕业设计(附源码)python医院预约挂号系统
    人脸识别:insightface自定义数据集制作 | 附练手数据集
  • 原文地址:https://blog.csdn.net/sjsjnsjnn/article/details/126364511