• 线程池的实现


    目录

    一、线程池的实现

    1.什么是线程池

    2.设计线程类

    3.设计线程池类

    4.运行

    5.RAII加锁改造

    二、利用单例模式改造线程池

    1.复习

    2.饿汉模式

    3.懒汉模式


    关于系统编程的知识我们已经学完了,最后我们需要利用之前写过的代码实现一个线程池,彻底结束系统编程部分。

    一、线程池的实现

    1.什么是线程池

    线程池是一种多线程处理形式,先将任务添加到队列,创建线程后线程池会自动启动线程处理这些任务。

    线程池有以下特点:

    • 线程池是一种线程使用模式,如果线程过多会,就会带来调度开销,进而影响缓存局部性和整体性能。
    • 线程池维护着多个线程,可以处理监督管理者分配的可并发执行的任务。
    • 线程池维护的多个线程在无任务时会处于阻塞等待状态,当有任务需要处理时,线程就会被唤醒,处理完成后继续阻塞等待而不销毁。这样的设置避免了处理短时间任务时调用系统调用创建与销毁线程的开销。
    • 线程池不仅能够保证内核的充分利用,还能防止过分调度。
    • 可用线程数量取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

    2.设计线程类

    设计线程池就需要一个线程类管理线程,之前封装的classThread我们增加了一个Context类,这次我们想只使用一个类就完成线程的管理。

    虽然需要重新设计,但是原来的函数有一部分也能使用。

    线程类的设计思路:

    • 成员变量包括线程的名字、线程的tid、外部定义的线程执行函数对象、线程执行函数需要传递的参数还有一个静态的线程的编号,静态变量需要在类外定义。
    • 对函数对象的类型重命名也可以增加代码可读性。
    • 构造函数只需要初始化线程名即可,线程的创建会在其他地方实现。
    • 我们创建一个start函数负责线程的创建,它有两个参数func和args,func是我们需要传入的线程处理函数,args是处理函数的参数。
    • 在线程创建的时候,具体的执行函数不要设为_func,这样会增加程序的耦合度。
    • 我们再创建一个start_routine函数负责线程的处理,由于它的返回值应该为void*,只有一个参数void* args,该函数必须设置为静态才能去掉默认的this指针参数。
    • start_routine的args还是要用于传递this指针,只是因为线程处理函数的特殊要求,我们才没有直接将其定义为成员函数。而且这个函数也应该定义为私有函数,防止使用者在类外调用该函数。
    • start_routine使用回调函数callback调用_func并传回结果。
    • 注意只有在回调函数中传递的是_args,另一个args是this指针。
    • 最后加上一个获取线程名的函数。

    最终实现的Thread.hpp

    1. #include
    2. #include
    3. #include
    4. #define NUM 64
    5. class Thread
    6. {
    7. typedef std::function<void*(void*)> func_t;
    8. public:
    9. //构造函数创建线程
    10. Thread()
    11. {
    12. //对线程进行规范化命名
    13. char buffer[NUM];
    14. snprintf(buffer, sizeof(buffer), "thread%d", _threadnum++);
    15. _name = buffer;
    16. }
    17. //启动线程
    18. void start(func_t func, void* args = nullptr)
    19. {
    20. //初始化这两个变量
    21. _args = args;
    22. _func = func;
    23. //创建线程,线程的处理函数为start_routine
    24. int n = pthread_create(&_tid, nullptr, start_routine, (void*)this);//将this指针传递到执行代码中
    25. //断言创建成功,这里也可以换成打印错误码的代码
    26. assert(n == 0);
    27. }
    28. //利用回调函数
    29. void* callback()
    30. {
    31. return _func(_args);
    32. }
    33. //获取线程的名字
    34. std::string threadname()
    35. {
    36. return std::string(_name);
    37. }
    38. void join()
    39. {
    40. int n = pthread_join(_tid, nullptr);
    41. assert(n == 0);
    42. }
    43. private:
    44. //处理函数的函数能只有一个确定的参数,所以只能定义为静态。
    45. static void* start_routine(void* args)//用args传递this指针
    46. {
    47. Thread* pt = static_cast(args);
    48. return pt->callback();
    49. }
    50. std::string _name;
    51. pthread_t _tid;
    52. func_t _func;
    53. void* _args;
    54. static int _threadnum;
    55. };
    56. int Thread::_threadnum = 1;

    我们测试该类

    test.cc

    1. #include
    2. #include
    3. #include"Thread.hpp"
    4. using namespace std;
    5. void* test(void* args)
    6. {
    7. char* p = (char*)args;
    8. while(1)
    9. {
    10. printf("%s\n", p);
    11. sleep(1);
    12. }
    13. }
    14. int main()
    15. {
    16. Thread t;
    17. t.start(test, (void*)"thread running");
    18. t.join();
    19. return 0;
    20. }

    线程可以正常运行:

    3.设计线程池类

    线程池类负责多线程的创建和维护,并且在基于阻塞队列的生产者消费者模型下运行。其中生产者是生成任务的线程,消费者是线程池维护的多个线程,数据结构为阻塞队列。线程池包括阻塞队列和所有消费者。

    线程池类的设计思路:

    • 成员变量包含线程池维护的线程数量_num、存放线程类Threads的vector容器_threads,存放任务的阻塞队列_queue、保证消费者线程互斥访问任务队列的锁_cmutex、让多线程同步的条件变量_cond、保证生产者互斥生产数据的锁_pmutex。
    • 首先构造中设置线程数量_num为缺省值,这样可以让用户决定线程的数量。然后将互斥锁和条件变量初始化,最后后创建_num个Thread对象并将指针尾插到_threads容器中。此时线程只有名字,还未被创建。
    • 然后实现析构函数,在其中将互斥锁和条件变量销毁,循环释放_threads容器中的所有线程回收并将Thread对象也全部delete掉。
    • 实现一个接口run(),通过该接口真正创建对应数量的新线程,并且开始执行。每成功创建一个线程且开始执行后,都要打印该线程开始运行的信息,比如thread1 start...。
    • run()创建线程需要循环_threads容器内Thread变量的start()函数,所有的线程就会启动。
    • 所有线程都执行静态handerTask函数,与以前一样,还是需要去掉this指针。
    • handler_task函数需要使用线程名和this指针,所以我们在前面创建一个ThreadData结构体存放它们,this指针放在ThreadPool* threadpool中,线程的名字放在_threadname中。创建线程就直接把这个结构体指针传过去就可以了。

    • 消费线程执行handler_task,该函数会从任务队列_task_queue中取任务执行,并且它们要按照一定顺序去访问,所以线程是同步和互斥的关系。
    • 因为handler_task是Thread类对象在执行,所以它中不能直接访问ThreadPool中的条件变量和锁。所以在类内需要提供公有的接口允许线程访问私有成员,允许其进行加锁,解锁,条件判断,以及取任务等操作。

    • 在访问任务队列的时候先加锁,如果任务队列为空则挂起等待,如果不为空则取走任务并处理任务。
    • 从任务队列中获取任务后,不再操作临界资源,所以解锁在前,任务处理在后。线程获取到任务时,会将任务放到自己的独立栈结构中。所以线程处理任务是相互独立的。
    • 线程处理完任务后,将在堆区存放当前线程属性的ThreadData对象释放掉。
    • 倒数第二步定义push函数用于推送任务,由于推送任务是操作临界资源,在推送任务到任务队列前加锁,推送完成后唤醒在条件变量_cond下等待的一个消费线程,再进行解锁。
    • 最后将从队列中取数据的pop函数定义,由于其在加锁代码里,素以不用考虑线程安全问题。

    4.运行

    此时我们再根据任务类型在main函数和handler_task函数中设置打印函数显示线程调度就可以了。我们设置主线程每一秒推送一个计算任务,计算任务直接使用之前的类。

    CalTask.hpp

    1. #include
    2. #include
    3. #include
    4. #define MAX_NUM 10
    5. //计算任务类
    6. class CalTask
    7. {
    8. typedef std::function<int(int,int,char)> func_t;
    9. public:
    10. //默认构造
    11. CalTask()
    12. {}
    13. //构造函数
    14. CalTask(int a, int b, char op, func_t func)
    15. :_a(a)
    16. ,_b(b)
    17. ,_op(op)
    18. ,_func(func)
    19. {}
    20. //仿函数
    21. std::string operator()()
    22. {
    23. int result = _func(_a, _b, _op);
    24. char buffer[64];
    25. snprintf(buffer, sizeof(buffer), "%d %c %d = %d", _a, _op, _b, result);
    26. std::string s(buffer);
    27. return s;
    28. }
    29. //显示任务
    30. std::string show_task()
    31. {
    32. char buffer[64];
    33. snprintf(buffer, sizeof(buffer), "%d %c %d = ?", _a, _op, _b);
    34. std::string s(buffer);
    35. return s;
    36. }
    37. private:
    38. func_t _func;
    39. int _a;
    40. int _b;
    41. char _op;
    42. };

    Thread.hpp

    1. #include
    2. #include
    3. #include
    4. #define NUM 64
    5. class Thread
    6. {
    7. typedef std::function<void*(void*)> func_t;
    8. public:
    9. //构造函数创建线程
    10. Thread()
    11. {
    12. //对线程进行规范化命名
    13. char buffer[NUM];
    14. snprintf(buffer, sizeof(buffer), "thread%d", _threadnum++);
    15. _name = buffer;
    16. }
    17. //启动线程
    18. void start(func_t func, void* args = nullptr)
    19. {
    20. //初始化这两个变量
    21. _args = args;
    22. _func = func;
    23. //创建线程,线程的处理函数为start_routine
    24. int n = pthread_create(&_tid, nullptr, start_routine, (void*)this);//将this指针传递到执行代码中
    25. //断言创建成功,这里也可以换成打印错误码的代码
    26. assert(n == 0);
    27. }
    28. //利用回调函数
    29. void* callback()
    30. {
    31. return _func(_args);
    32. }
    33. //获取线程的名字
    34. std::string threadname()
    35. {
    36. return std::string(_name);
    37. }
    38. void join()
    39. {
    40. int n = pthread_join(_tid, nullptr);
    41. assert(n == 0);
    42. }
    43. private:
    44. //处理函数的函数能只有一个确定的参数,所以只能定义为静态。
    45. static void* start_routine(void* args)//用args传递this指针
    46. {
    47. Thread* pt = static_cast(args);
    48. return pt->callback();
    49. }
    50. std::string _name;
    51. pthread_t _tid;
    52. func_t _func;
    53. void* _args;
    54. static int _threadnum;
    55. };
    56. int Thread::_threadnum = 1;

    Threadpool.hpp

    1. #include
    2. #include
    3. #include
    4. #define THREAD_NUM 10
    5. //前面加上声明
    6. template <class T>
    7. class ThreadPool;
    8. //线程数据类
    9. template <class T>
    10. class ThreadData
    11. {
    12. public:
    13. ThreadPool* _pthreadpool;//线程池的this指针
    14. std::string _threadname;//线程的名字
    15. //构造函数
    16. ThreadData(ThreadPool* tp, std::string name)
    17. :_pthreadpool(tp)
    18. ,_threadname(name)
    19. {}
    20. };
    21. //线程池
    22. template <class T>
    23. class ThreadPool
    24. {
    25. public:
    26. //构造函数
    27. ThreadPool(int num = THREAD_NUM)
    28. :_num(num)
    29. {
    30. pthread_mutex_init(&_cmutex, nullptr);//初始化消费互斥锁
    31. pthread_mutex_init(&_pmutex, nullptr);//初始化生产互斥锁
    32. pthread_cond_init(&_cond, nullptr);//初始化条件变量
    33. //创建多个线程
    34. for(size_t i = 0; i < _num; ++i)
    35. {
    36. _threads.push_back(new Thread());
    37. }
    38. }
    39. //析构函数
    40. ~ThreadPool()
    41. {
    42. pthread_mutex_destroy(&_cmutex);//销毁消费互斥锁
    43. pthread_mutex_destroy(&_pmutex);//销毁生产互斥锁
    44. pthread_cond_destroy(&_cond);//销毁条件变量
    45. //销毁多个线程
    46. for(size_t i = 0; i < _num; ++i)
    47. {
    48. _threads[i]->join();
    49. delete _threads[i];
    50. }
    51. }
    52. //将所有线程启动
    53. void run()
    54. {
    55. for(size_t i = 0; i < _num; ++i)
    56. {
    57. //由于线程函数需要使用线程池类内的函数和每一个线程的名字,所以将它们合起来构造一个线程数据类传递给线程操作函数
    58. ThreadData* p = new ThreadData(this, _threads[i]->threadname());
    59. _threads[i]->start(handler_task, (void*)p);//这里也可以设计一个类
    60. std::string s(p->_threadname);
    61. s += " start...\n";
    62. std::cout << s;
    63. }
    64. }
    65. //向线程池推送任务
    66. void push(const T& data)
    67. {
    68. pthread_mutex_lock(&_pmutex);
    69. _task_queue.push(data);
    70. pthread_cond_signal(&_cond);
    71. pthread_mutex_unlock(&_pmutex);
    72. }
    73. //消费线程取任务,加锁解锁已经在消费线程处理函数里进行了,不需要注意线程安全
    74. T pop()
    75. {
    76. T data = _task_queue.front();
    77. _task_queue.pop();
    78. return data;
    79. }
    80. //静态成员函数需要访问的非静态成员接口
    81. bool isQueueEmpty() {return _task_queue.empty();}//判断任务队列是否为空
    82. void lockQueue() {pthread_mutex_lock(&_cmutex);}//给任务队列加锁
    83. void unlockQueue() {pthread_mutex_unlock(&_cmutex);}//给任务队列解锁
    84. void threadWait() {pthread_cond_wait(&_cond,&_cmutex);}//将线程放入条件变量的等待队列中
    85. private:
    86. //消费线程的处理函数
    87. static void* handler_task(void* args)
    88. {
    89. ThreadData* p = (ThreadData*)args;
    90. while(1)
    91. {
    92. p->_pthreadpool->lockQueue();
    93. //如果任务队列为空,消费者进程会被加入到条件变量的阻塞队列中
    94. while(p->_pthreadpool->isQueueEmpty())
    95. {
    96. p->_pthreadpool->threadWait();
    97. }
    98. T data = p->_pthreadpool->pop();
    99. p->_pthreadpool->unlockQueue();
    100. printf("%s接受了任务%s并处理完成,结果为:%s\n", p->_threadname.c_str(),
    101. data.show_task().c_str(), data().c_str());
    102. }
    103. delete p;
    104. return nullptr;
    105. }
    106. int _num;//维护的线程数量
    107. std::vector _threads;//管理多个线程对象的容器
    108. std::queue _task_queue;//任务队列
    109. pthread_mutex_t _cmutex;//消费者互斥锁
    110. pthread_cond_t _cond;//条件变量
    111. pthread_mutex_t _pmutex;//生成任务时的互斥锁
    112. };

    test.cc

    1. #include
    2. #include
    3. #include"CalTask.hpp"
    4. #include"Thread.hpp"
    5. #include"Threadpool.hpp"
    6. using namespace std;
    7. //计算器函数
    8. const string ops = "+-*/%";
    9. int calculate(int a, int b, char op)
    10. {
    11. int result = 0;
    12. switch(op)
    13. {
    14. case '+':
    15. result = a + b;
    16. break;
    17. case '-':
    18. result = a - b;
    19. break;
    20. case '*':
    21. result = a * b;
    22. break;
    23. case '/':
    24. {
    25. if(b == 0)
    26. cerr << "除数不能为0\n";
    27. else
    28. result = a / b;
    29. }
    30. break;
    31. case '%':
    32. {
    33. if(b == 0)
    34. cerr << "取模的数字不能为0\n";
    35. else
    36. result = a % b;
    37. }
    38. break;
    39. default:
    40. break;
    41. }
    42. return result;
    43. }
    44. int main()
    45. {
    46. srand((unsigned int)time(nullptr));
    47. ThreadPool* tp = new ThreadPool();
    48. tp->run();
    49. for(;;)
    50. {
    51. //
    52. int a = rand()%10;
    53. int b = rand()%10;
    54. char op = ops[rand()%ops.size()];
    55. CalTask task(a, b, op, calculate);
    56. tp->push(task);
    57. printf("主线程推送任务:%d %c %d = ?\n", a, op, b);
    58. sleep(1);
    59. }
    60. return 0;
    61. }

    运行结果:

    5.RAII加锁改造

    我们之前写了一个LockGuard类,我们可以将这个类在task_handler的加锁代码中就可以用起来。

    1. #include
    2. class mutex
    3. {
    4. public:
    5. //构造函数
    6. mutex(pthread_mutex_t* p = nullptr)
    7. :_pmutx(p)
    8. {}
    9. //加锁
    10. void lock()
    11. {
    12. pthread_mutex_lock(_pmutx);
    13. }
    14. //解锁
    15. void unlock()
    16. {
    17. pthread_mutex_unlock(_pmutx);
    18. }
    19. private:
    20. pthread_mutex_t* _pmutx;
    21. };
    22. class LockGuard
    23. {
    24. public:
    25. LockGuard(pthread_mutex_t* lock_p):_mutex(lock_p)
    26. {
    27. _mutex.lock();//构造函数内加锁
    28. }
    29. ~LockGuard()
    30. {
    31. _mutex.unlock();//析构函数内解锁
    32. }
    33. private:
    34. mutex _mutex;
    35. };

    LockGuard构造函数加锁,析构函数解锁。通过{}控制该变量的生命周控制加锁。

    由于该函数是由线程执行的,锁是线程池的私有变量,所以也需要增加一个函数获取锁。

    最后改造的handler_task代码如下:

    代码正常运行。

    二、利用单例模式改造线程池

    1.复习

    饿汉模式和懒汉模式都是单例模式的实现方式,具体的介绍可以看这篇博客的第二章:C++特殊类设计及类型转换_聪明的骑士的博客-CSDN博客

    现在我们要将线程池改为这两种单例模式。

    2.饿汉模式

    饿汉模式:不管以后会不会使用单例对象,只要程序一启动,程序就会先创建一个唯一的实例对象然后再执行其他代码。

    首先,增加一个静态对象保证单例的唯一性,静态变量需要在根据自己的类型在类外初始化。

    然后需要将构造函数设为私有,并且不允许生成拷贝构造和赋值运算符重载。

    最后,添加一个静态函数GetInstance()获取单例的地址。

    此时,更改一下主线程的代码:

    1. int main()
    2. {
    3. srand((unsigned int)time(nullptr));
    4. ThreadPool::GetInstance()->run();
    5. for(;;)
    6. {
    7. int a = rand()%10;
    8. int b = rand()%10;
    9. char op = ops[rand()%ops.size()];
    10. CalTask task(a, b, op, calculate);
    11. ThreadPool::GetInstance()->push(task);
    12. printf("主线程推送任务:%d %c %d = ?\n", a, op, b);
    13. sleep(1);
    14. }
    15. return 0;
    16. }

    程序正常运行。

    3.懒汉模式

    懒汉模式:单例只有在第一次被使用到时才被建立,就像一个懒汉一样,什么事都拖到截止日才干。

    首先,在原来饿汉模式的基础上将单例对象改为单例对象指针,并且设置为空。

    然后,为了保证单例的建立是线程安全的,还要增加一个C++11提供的锁并在类外初始化(需要包含mutex.h头文件,这里用C++标准提供的锁是因为它操作更方便)。

    最后,使用之前的双检查加锁方式重新设计一下获取单例指针的接口。

    主线程代码不需要改,直接运行就可以跑起来了。

    最后,所有Linux系统编程的知识就讲解完毕了,接下来我们要进行网络编程的学习。

  • 相关阅读:
    【思想篇-能力】新人成长
    C++——stack和queue的使用和OJ练习题
    vue3中子级页面绑定调用父级页面得自定义事件
    RPC - gRPC简单的demo - 学习/实践
    Switch case 使用及嵌套语法
    游戏优化之空间划分
    系统数据数据和信息
    C++ 2019-2022 CSP_J 复赛试题横向维度分析(上)
    轻松入门自然语言处理系列 专题7 基于FastText的文本分类
    python 替换字符串中 windows非法字符 到对应的 全角合法字符
  • 原文地址:https://blog.csdn.net/qq_65285898/article/details/132743443