• Linux 线程池&单例模式&读写锁&自旋锁


                     

           历经整整一个月终于到linux系统知识的最后一篇博客了,在这期间博主承认有偷懒几天~这篇博客写完后,接下来就是C++的高阶数据结构了。等博主把网络编程和MySql学好之后再继续写~今天主要介绍线程池、单例模式、读者写者模型、悲观锁和自旋锁的区别。

    目录

    线程池

    为什么要有线程池?

    代码测试

    thread_pool.hpp

    task.hpp

    main.cc

    Makefile

    单例模式

    什么是单例模式?

    什么是设计模式?

    单例模式特点

    饿汉实现方式和懒汉实现方式

    饿汉模式实现单例

    懒汉模式方式单例模式

    懒汉模式实现线程池版本代码

    signal_pool.hpp

    task.hpp

    main.cc

    读者写者模型

    基本理论

    函数接口 

    pthread_rwlock_init

    pthread_rwlock_destroy

    pthread_rwlock_wrlock

    pthread_rwlock_rdlock

    phread_rwlock_unlock

    如何理解?伪代码

    挂起等待的锁 vs 自旋锁

    自旋锁函数接口

    pthread_spin_init

    pthread_spin_destroy

    pthread_spin_lock

    pthread_spin_unlock


    线程池

    为什么要有线程池?

    我们先类比一下内存池:

           我们如果频繁向OS申请小块空间,OS就要在底层做很多动作,比如进程身份状态的变化,OS执行内存处理算法。这些对于用户层来说都是没必要的,但确实是耗时间的!所以引入了内存池的概念,趁着OS好的时候,一次申请一大块空间,这块空间在用户层进行管理,这样当我们再次申请空间时,就不需要向OS去要了,直接在用户层拿就好了,这样效率大大提高。

    再来看线程池:

           我们发现当多个任务到来时,OS要创建线程,但是临时创建的话效率肯定是比较低的。所以要提前创建好线程,保存在在线程池中。

    代码测试

    thread_pool.hpp

    1. #pragma once
    2. #include<iostream>
    3. #include<pthread.h>
    4. #include<queue>
    5. #include<string>
    6. using namespace std;
    7. namespace ns_threadpool
    8. {
    9. const int g_num = 3; //线程池中线程的数目
    10. template<class T>
    11. class ThreadPool
    12. {
    13. private:
    14. int _num; //线程池中线程的数目
    15. queue<T> _task_queue;//该成员是一个临界资源
    16. pthread_mutex_t _mtx;
    17. pthread_cond_t _cond;
    18. public:
    19. void Lock()
    20. {
    21. pthread_mutex_lock(&_mtx);
    22. }
    23. void Unlock()
    24. {
    25. pthread_mutex_unlock(&_mtx);
    26. }
    27. void Wait()
    28. {
    29. pthread_cond_wait(&_cond, &_mtx);
    30. }
    31. void WakeUp()
    32. {
    33. pthread_cond_signal(&_cond);
    34. }
    35. bool IsEmpty()
    36. {
    37. return _task_queue.empty();
    38. }
    39. public:
    40. ThreadPool(int num = g_num)
    41. :_num(num)
    42. {
    43. pthread_mutex_init(&_mtx, nullptr);
    44. pthread_cond_init(&_cond, nullptr);
    45. }
    46. //在类中要让线程执行类内成员方法(参数个数匹配问题,只有一个参数),是不可行的!
    47. //必须让线程执行静态方法
    48. static void* Rountine(void* args)
    49. {
    50. pthread_detach(pthread_self());//线程分离
    51. ThreadPool<T>* tp = (ThreadPool<T>*)args;
    52. while(true)
    53. {
    54. tp->Lock();
    55. while(tp->IsEmpty())
    56. {
    57. //任务队列为空,线程该做些什么呢?
    58. tp->Wait();
    59. }
    60. //到这里,该任务队列中一定有任务了
    61. T t;
    62. tp->PopTask(&t);
    63. tp->Unlock();
    64. t();
    65. }
    66. }
    67. void InitThreadPool()
    68. {
    69. pthread_t tid;
    70. for(int i = 0; i < _num; i++)
    71. {
    72. pthread_create(&tid, nullptr, Rountine, (void*)this);
    73. }
    74. }
    75. void PushTask(const T& in)
    76. {
    77. Lock();
    78. _task_queue.push(in);
    79. Unlock();
    80. WakeUp();
    81. }
    82. //由于queue大小可以动态增长,这里不考虑满了就等待的的情况
    83. //这里不能加锁,否则就成死锁了,Rountine也有锁,但是这里也是安全的~
    84. void PopTask(T* out)
    85. {
    86. *out = _task_queue.front();//尾插头出
    87. _task_queue.pop(); //尾插头出
    88. }
    89. ~ThreadPool()
    90. {
    91. pthread_mutex_destroy(&_mtx);
    92. pthread_cond_destroy(&_cond);
    93. }
    94. };
    95. }

    task.hpp

    1. #pragma once
    2. #include <iostream>
    3. #include <pthread.h>
    4. #include<map>
    5. #include<functional>
    6. namespace ns_task
    7. {
    8. class Task
    9. {
    10. public:
    11. Task()
    12. {}
    13. Task(int x, int y, char op)
    14. : _x(x), _y(y), _op(op)
    15. {}
    16. int Run()
    17. {
    18. int res = 0;
    19. switch (_op)
    20. {
    21. case '+':
    22. res = _x + _y;
    23. break;
    24. case '-':
    25. res = _x - _y;
    26. break;
    27. case '*':
    28. res = _x * _y;
    29. break;
    30. case '/':
    31. res = _x / _y;
    32. break;
    33. case '%':
    34. res = _x % _y;
    35. break;
    36. default:
    37. cout << "bug??" << endl;
    38. break;
    39. }
    40. cout << "当前任务正在被: " << pthread_self() << " 处理: ";
    41. cout << _x << _op << _y << "=" << res << endl;
    42. return res;
    43. }
    44. int operator()()
    45. {
    46. return Run();
    47. }
    48. private:
    49. int _x;
    50. int _y;
    51. char _op; //+-*/%
    52. };
    53. }

    main.cc

    1. #include "thread_pool.hpp"
    2. #include "task.hpp"
    3. #include<time.h>
    4. #include<stdlib.h>
    5. #include<unistd.h>
    6. using namespace ns_task;
    7. using namespace ns_threadpool;
    8. int main()
    9. {
    10. ThreadPool<Task>* tp = new ThreadPool<Task>(3);
    11. tp->InitThreadPool();
    12. srand((unsigned int)time(0));
    13. while(true)
    14. {
    15. //生产任务
    16. int x = rand()%20 + 1;
    17. int y = rand()%10 + 1;
    18. char op = "+-*/%"[rand()%5];
    19. Task t(x, y, op);
    20. //放任务
    21. tp->PushTask(t);
    22. sleep(1);
    23. }
    24. return 0;
    25. }

    Makefile

    1. main:main.cc
    2. g++ -o $@ $^ -std=c++11 -lpthread
    3. .PHONY:clean
    4. clean:
    5. rm -f main

    测试结果:

    这时候我们发现,我们不断往任务队列中塞任务,我们在线程池中创建的多个线程不断去处理。 

    单例模式

    什么是单例模式?

    单例模式是一种 "经典的, 常用的, 常考的" 设计模式

    什么是设计模式?

           IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重.。为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是设计模式。

    单例模式特点

    某些类, 只应该具有一个对象(实例), 就称之为单例。
    例如一个男人只能有一个媳妇.
    在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据。

    饿汉实现方式和懒汉实现方式

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

    总结:懒汉方式最核心的思想是 "延时加载",从而能够优化服务器的启动速度。这和我们之前讲过的内存申请和内存使用及写时拷贝的概念类似。

    饿汉模式实现单例

    懒汉模式方式单例模式

     通过Singleton 这个包装类来使用T对象, 则一个进程中只有一个T对象的实例。

    注意:

    存在一个严重的问题, 线程不安全。第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例。

    懒汉模式实现线程池版本代码

    signal_pool.hpp

    1. #pragma once
    2. #include <iostream>
    3. #include <pthread.h>
    4. #include <queue>
    5. #include <string>
    6. using namespace std;
    7. namespace ns_threadpool
    8. {
    9. const int g_num = 3; //线程池中线程的数目
    10. template <class T>
    11. class ThreadPool
    12. {
    13. private:
    14. int _num; //线程池中线程的数目
    15. queue<T> _task_queue; //该成员是一个临界资源
    16. pthread_mutex_t _mtx;
    17. pthread_cond_t _cond;
    18. static ThreadPool<T>* _ins;
    19. private:
    20. //构造函数必须得实现,而且设置为私有
    21. ThreadPool(int num = g_num)
    22. : _num(num)
    23. {
    24. pthread_mutex_init(&_mtx, nullptr);
    25. pthread_cond_init(&_cond, nullptr);
    26. }
    27. //拷贝构造和赋值设置为私有
    28. ThreadPool(const ThreadPool<T>& tp) = delete;
    29. ThreadPool<T> operator=(const ThreadPool<T>& tp) = delete;
    30. public:
    31. static ThreadPool<T>* GetInstance()
    32. {
    33. //静态锁不用手动初始化和销毁
    34. static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    35. //当前单例对象还没有被创建
    36. if(_ins == nullptr) //双判定,减少锁的争用,提高单例的效率
    37. {
    38. pthread_mutex_lock(&lock);
    39. if(_ins == nullptr)
    40. {
    41. _ins = new ThreadPool<T>();
    42. _ins->InitThreadPool(); //创建线程
    43. cout << "首次加载对象" << endl;
    44. }
    45. pthread_mutex_unlock(&lock);
    46. }
    47. return _ins;
    48. }
    49. //在类中要让线程执行类内成员方法(参数个数匹配问题,只有一个参数),是不可行的!
    50. //必须让线程执行静态方法
    51. static void *Rountine(void *args)
    52. {
    53. pthread_detach(pthread_self()); //线程分离
    54. ThreadPool<T> *tp = (ThreadPool<T> *)args;
    55. while (true)
    56. {
    57. tp->Lock();
    58. while (tp->IsEmpty())
    59. {
    60. //任务队列为空,线程该做些什么呢?
    61. tp->Wait();
    62. }
    63. //到这里,该任务队列中一定有任务了
    64. T t;
    65. tp->PopTask(&t);
    66. tp->Unlock();
    67. t();
    68. }
    69. }
    70. void InitThreadPool()
    71. {
    72. pthread_t tid;
    73. for (int i = 0; i < _num; i++)
    74. {
    75. pthread_create(&tid, nullptr, Rountine, (void *)this);
    76. }
    77. }
    78. void PushTask(const T& in)
    79. {
    80. Lock();
    81. _task_queue.push(in);
    82. Unlock();
    83. WakeUp();
    84. }
    85. //由于queue大小可以动态增长,这里不考虑满了就等待的的情况
    86. //这里不能加锁,否则就成死锁了,Rountine也有锁,但是这里也是安全的~
    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. public:
    98. void Lock()
    99. {
    100. pthread_mutex_lock(&_mtx);
    101. }
    102. void Unlock()
    103. {
    104. pthread_mutex_unlock(&_mtx);
    105. }
    106. void Wait()
    107. {
    108. pthread_cond_wait(&_cond, &_mtx);
    109. }
    110. void WakeUp()
    111. {
    112. pthread_cond_signal(&_cond);
    113. }
    114. bool IsEmpty()
    115. {
    116. return _task_queue.empty();
    117. }
    118. };
    119. template<class T>
    120. ThreadPool<T>* ThreadPool<T>::_ins = nullptr;
    121. }

    task.hpp

    1. #pragma once
    2. #include <iostream>
    3. #include <pthread.h>
    4. #include<map>
    5. #include<functional>
    6. namespace ns_task
    7. {
    8. class Task
    9. {
    10. public:
    11. Task()
    12. {}
    13. Task(int x, int y, char op)
    14. : _x(x), _y(y), _op(op)
    15. {}
    16. int Run()
    17. {
    18. int res = 0;
    19. switch (_op)
    20. {
    21. case '+':
    22. res = _x + _y;
    23. break;
    24. case '-':
    25. res = _x - _y;
    26. break;
    27. case '*':
    28. res = _x * _y;
    29. break;
    30. case '/':
    31. res = _x / _y;
    32. break;
    33. case '%':
    34. res = _x % _y;
    35. break;
    36. default:
    37. cout << "bug??" << endl;
    38. break;
    39. }
    40. cout << "当前任务正在被: " << pthread_self() << " 处理: ";
    41. cout << _x << _op << _y << "=" << res << endl;
    42. return res;
    43. }
    44. int operator()()
    45. {
    46. return Run();
    47. }
    48. private:
    49. int _x;
    50. int _y;
    51. char _op; //+-*/%
    52. };
    53. }

    main.cc

    1. #include "thread_pool.hpp"
    2. #include "task.hpp"
    3. #include<time.h>
    4. #include<stdlib.h>
    5. #include<unistd.h>
    6. using namespace ns_task;
    7. using namespace ns_threadpool;
    8. int main()
    9. {
    10. cout << "当前正在运行我的进程其他代码..." << endl;
    11. cout << "当前正在运行我的进程其他代码..." << endl;
    12. cout << "当前正在运行我的进程其他代码..." << endl;
    13. cout << "当前正在运行我的进程其他代码..." << endl;
    14. cout << "当前正在运行我的进程其他代码..." << endl;
    15. sleep(3);
    16. while(true)
    17. {
    18. //生产任务
    19. int x = rand()%20 + 1;
    20. int y = rand()%10 + 1;
    21. char op = "+-*/%"[rand()%5];
    22. Task t(x, y, op);
    23. Task t1(rand()%20+1, rand()%10+1, "+-*/%"[rand()%5]);
    24. ThreadPool<Task>::GetInstance()->PushTask(t);
    25. //单例模式本身会在任何场景,任何环境下使用
    26. //GetInstance():被多线程重入,进而导致线程安全问题
    27. //所以要加锁
    28. cout << ThreadPool<Task>::GetInstance() << endl;
    29. sleep(1);
    30. }
    31. return 0;
    32. }

    运行结果:

    我们发现每次都是一个只有对象,这个对象的线程池里面有多个线程执行任务。

    读者写者模型

    基本理论

    函数接口 

    pthread_rwlock_init

    pthread_rwlock_t *restrict rwlock:传入定义的pthread_rwlock_t变量的地址 

    const pthread_rwlockattr_t *restrict attr:设置属性,我们不关心,传nullptr就好了

    pthread_rwlock_destroy

    pthread_rwlock_t* rwlock:传入变量的地址,进行释放锁资源 

    pthread_rwlock_wrlock

    以写方式加锁 

    pthread_rwlock_rdlock

    以读者身份加锁 

    phread_rwlock_unlock

    解锁,读者写者以统一方式解锁。

    如何理解?伪代码

    优先级

    读者优先:读者和写者同时到来的时候,我们让读者先进入访问。

    写者优先:当读者和写者同时到来的时候,比当前写者晚来的所有的读者,都不要进入临界区访问了,等临界区中没有读者的时候,让写者先写入。 

    注意:读者多,写着少的问题,是存在"饥饿问题",但是,"饥饿问题"是一个中性词。

    挂起等待的锁 vs 自旋锁

    我们先找一个现实场景:

           对于临界资源任务处理时间比较短就不适合使用挂起等待的锁,因为可能还没有线程挂起等待,别的线程就释放锁了,这时候线程还得继续挂起然后立刻被唤醒,这是有一定成本的! 

           如果对于处理任务比较长的时候,不适合自旋锁,自旋锁不停地循环检测锁的状态,长时间的话消耗CPU资源也是很大的。

    所以针对不同的场景,要选用合适的锁

    线程如何得知,自己在临界资源中呆多长时间?

    线程不知道!!程序员知道!所以是程序员选择锁的使用!

    自旋锁函数接口

    pthread_spin_init

    pthread_spinlock_t* lock:传入定义的自旋锁的变量的地址

    int pshared:是否进程间共享,我们一般设置成0

    pthread_spin_destroy

     pthread_spinlock_t* lock:传入定义的自旋锁的变量的地址,释放锁资源

    pthread_spin_lock

     pthread_spin_lock:传入定义的自旋锁的变量的地址,进行加锁

    pthread_spin_unlock

      pthread_spin_lock:传入定义的自旋锁的变量的地址,进行解锁

    我们发现自旋锁和互斥锁的使用几乎一模一样,有了之前的基础,我们就可以使用起来自旋锁。

    看到这里,给博主点个赞吧~

                            

  • 相关阅读:
    微服务(二) php laravel 用户客户端
    Tomcat服务器的简介
    springfox及springdoc
    YOLO目标检测——VOC2007数据集+已标注VOC格式标签下载分享
    Kafka3.2教程(一)消息队列与Kafka原理
    [科研琐事] 安装服务器的二三事
    失眠睡不着如何调理
    计算机毕业设计Java婚纱摄影网站(源码+系统+mysql数据库+lw文档)
    [2022 牛客多校2 E] Falfa with Substring (二项式反演 NTT)
    软件工程--软件过程学习笔记
  • 原文地址:https://blog.csdn.net/qq_58724706/article/details/125470866