前言:

目前网上的c++线程池资源多是使用老版本或者使用系统接口实现,使用c++ 11新特性的不多,最近研究了一下,实现一个简单版本,可实现任意任意参数函数的调用以及获得返回值。

0 前置知识

首先介绍一下用到的c++新特性

  1. 可变参数模板:利用这一特性实现任意参数的传递
  2. bind函数,lambda表达式: 用于将带参数的函数封装为不带形参和无返回值的函数,统一接口
  3. forward: 完美转发,防止在函数封装绑定时改变形参的原始属性(引用,常量等属性)
  4. shared_ptr, unique_ptr:智能指针,程序结束自动析构,不用手动管理资源,省心省力
  5. thread:c++11 引入的多线程标准库,完美跨平台
  6. future:期物,用于子线程结束后获取结果
  7. package_task: 异步任务包装模板,可以包装函数用于其它线程.有点类似与function
  8. function: 函数包装模板库,可以理解为将不同类型但形参和返回值相同的函数统一的接口
  9. queue,vecort: 向量,队列
  10. mutex: c++ 11引入的互斥锁对象
  11. condition_variable: c++ 11引入的条件变量,用于控制线程阻塞
  12. atmoic:原子变量,++,--,+=,-=这些操作时原子类型的,防止读取写于入失败

1 理论知识

问题0:线程运行完函数后自动就被系统回收了,怎么才能实现复用呢

:刚开始我也是比较疑惑,以为有个什么状态方法可以调用,在线程结束被销毁前阻塞住,从而接取下一个任务,实现复用,其实并非如此,线程池实现的原理是,让线程执行一个死循环任务,当任务队列为空时,就让他阻塞防止资源浪费,当有任务时,解除阻塞,让线程向下执行,当执行完当前函数后,又会再次运行到死循环的的上方,继续向下执行,从而周而复始的不断接任务--完成任务--接任务的循环,这里可以设置一个变量来控制,当想销毁线程池的时候,让死循环不再成立,当该线程执行完当前函数后,退出循环,从而销毁线程,思路很精妙

问题1:传入的函数多种多样,怎么能实现一个统一调用的模式呢

:用过c++多线程的就应该知道,我们在创建线程时,需要给thread传递函数地址和参数,但是我们的任务参数是多种多样的,数量不一,这时候,我们就需要使用可变参数模板将函数经过两次封装,封装为统一格式,第一次封装,封装为不含有形参的函数,即参数绑定,但此时是有返回值的,第二次封装,将函数的返回值也去除,这样我们就能使用void()这种统一的形式去调用了。第一次封装我们使用bind()函数将多个参数的函数封装为没有形参的package_task对象,为什么呢,因为package_task对象可以通过get_future得到future对象,然后future对象可以通过get方法获取返回值,这样我们第二步,就能直接把返回值也去掉了。

说了这么多,有点绕,对于没怎么使用过新特性的同学来说,可能云雾缭绕,其实真正想明白这两个问题,线程池的理论问题就解决了

2代码实现

总共包含5个文件,两个头文件,3个源文件,

2.1 任务队列头文件和实现

这两个文件是实现任务队列的,其实很简单,两个方法,一个放入任务,一个取出任务,放入任务就放我们封装后的

  1. /** Created by Jiale on 2022/3/14 10:19.
  2. * Decryption: 任务队列头文件
  3. **/
  4. #ifndef THREADPOOL_TASKQUEUE_H
  5. #define THREADPOOL_TASKQUEUE_H
  6. #include <queue>
  7. #include <functional>
  8. #include <mutex>
  9. #include <future>
  10. #include <iostream>
  11. class TaskQueue {
  12. public:
  13. using Task = std::function<void()>; // 任务类
  14. template<typename F, typename ...Args>
  15. auto addTask(F &f, Args &&...args) -> std::future<decltype(f(args...))>; // 添加任务
  16. Task takeTask(); // 取任务
  17. bool empty() {return taskQueue.empty();}
  18. private:
  19. std::mutex taskQueueMutex; // 任务队列互斥锁
  20. std::queue<Task> taskQueue; // 任务队列
  21. };
  22. template <typename F, typename ...Args> // 可变参数模板,模板必须在头文件定义
  23. auto TaskQueue::addTask(F &f, Args &&...args)-> std::future<decltype(f(args...))> {
  24. using RetType = decltype(f(args...)); // 获取函数返回值类型
  25. // 将函数封装为无形参的类型 std::bind(f, std::forward<Args>(args)...):将参数与函数名绑定
  26. // packaged_task<RetType()>(std::bind(f, std::forward<Args>(args)...)); 将绑定参数后的函数封装为只有返回值没有形参的任务对象,这样就能使用get_future得到future对象,然后future对象可以通过get方法获取返回值了
  27. // std::make_shared<std::packaged_task<RetType()>>(std::bind(f, std::forward<Args>(args)...)); 生成智能指针,离开作用域自动析构
  28. auto task = std::make_shared<std::packaged_task<RetType()>>(std::bind(f, std::forward<Args>(args)...));
  29. std::lock_guard<std::mutex> lockGuard(taskQueueMutex); // 插入时上锁,防止多个线程同时插入
  30. // 将函数封装为无返回无形参类型,通过lamdba表达式,调用封装后的函数,注意,此时返回一个无形参无返回值的函数对象
  31. taskQueue.emplace([task]{(*task)();});
  32. return task->get_future();
  33. }
  34. #endif //THREADPOOL_TASKQUEUE_H
  1. /** Created by Jiale on 2022/3/14 10:19.
  2. * Decryption: 任务队列源文件
  3. **/
  4. #include "include/TaskQueue.h"
  5. /**
  6. * 从任务队列中取任务
  7. * @return 取出的任务
  8. */
  9. TaskQueue::Task TaskQueue::takeTask() {
  10. Task task;
  11. std::lock_guard<std::mutex> lockGuard(taskQueueMutex); // 上锁
  12. if (!taskQueue.empty()) {
  13. task = std::move(taskQueue.front()); // 取出任务
  14. taskQueue.pop(); // 将任务从队列中删除
  15. return task;
  16. }
  17. return nullptr;
  18. }

可以看出,代码不多,就是一个简单的放入任务,取出任务,但是如果没接触过这种写法的时候还是比较难想的,我把那句难理解的代码拆成三部分

2.2 线程池代码实现

  1. /** Created by Jiale on 2022/3/14 10:42.
  2. * Decryption: 线程池头文件
  3. **/
  4. #ifndef THREADPOOL_THREADPOOL_H
  5. #define THREADPOOL_THREADPOOL_H
  6. #include <atomic>
  7. #include <thread>
  8. #include <condition_variable>
  9. #include "TaskQueue.h"
  10. class ThreadPool {
  11. std::atomic<int> threadNum{}; // 最小线程数
  12. std::atomic<int> busyThreadNum; // 忙线程数
  13. std::condition_variable notEmptyCondVar; // 判断任务队列是否非空
  14. std::mutex threadPoolMutex; // 线程池互斥锁
  15. bool shutdown; // 线程池是否启动
  16. std::unique_ptr<TaskQueue> taskQueue; // 任务队列
  17. std::vector<std::shared_ptr<std::thread>> threadVec; // 线程池
  18. public:
  19. explicit ThreadPool(int threadNum = 5); // 创建线程池
  20. ~ThreadPool(); // 销毁线程池
  21. template <typename F, typename ...Args>
  22. auto commit(F &f, Args &&...args) -> decltype(taskQueue->addTask(f, std::forward<Args>(args)...)); // 提交一个任务
  23. void worker();
  24. };
  25. template <typename F, typename ...Args> // 可变参数模板
  26. auto ThreadPool::commit(F &f, Args &&...args) -> decltype(taskQueue->addTask(f, std::forward<Args>(args)...)){
  27. // 这个目的就是把接收的参数直接转发给我们上面写的addTask方法,这样,就可以对使用者隐藏TaskQueue的细节,只向用户暴露ThreadPool就行
  28. auto ret = taskQueue->addTask(f, std::forward<Args>(args)...);
  29. notEmptyCondVar.notify_one();
  30. return ret;
  31. }
  32. #endif //THREADPOOL_THREADPOOL_H
  1. /** Created by Jiale on 2022/3/14 10:42.
  2. * Decryption:线程池源文件
  3. **/
  4. #include "include/ThreadPool.h"
  5. ThreadPool::ThreadPool(int threadNum) : taskQueue(std::make_unique<TaskQueue>()), shutdown(false), busyThreadNum(0) {
  6. this->threadNum.store(threadNum);
  7. for (int i = 0; i < this->threadNum; ++i) {
  8. threadVec.push_back(std::make_shared<std::thread>(&ThreadPool::worker, this)); // 创建线程
  9. threadVec.back()->detach(); // 创建线程后detach,与主线程脱离
  10. }
  11. }
  12. ThreadPool::~ThreadPool() {
  13. shutdown = true; // 等待线程执行完,就不在去队列取任务
  14. }
  15. void ThreadPool::worker() {
  16. while (!shutdown) {
  17. std::unique_lock<std::mutex> uniqueLock(threadPoolMutex);
  18. notEmptyCondVar.wait(uniqueLock, [this] { return !taskQueue->empty() || shutdown; }); // 任务队列为空,阻塞在此,当任务队列不是空或者线程池关闭时,向下执行
  19. auto currTask = std::move(taskQueue->takeTask()); // 取出任务
  20. uniqueLock.unlock();
  21. ++busyThreadNum;
  22. currTask(); // 执行任务
  23. --busyThreadNum;
  24. }
  25. }

2.3 测试

线程池设计好了,我们进行测试,如果我们开5个子线程,处理20个任务,那么,应该有5个线程ID,且是5个线程并发执行的,我们在测试函数里睡眠2秒,那么,总的时间应该是8秒执行完

  1. #include <iostream>
  2. #include <thread>
  3. #include <future>
  4. #include "ThreadPool.h"
  5. using namespace std;
  6. mutex mut;
  7. int func(int x) {
  8. auto now = time(nullptr);
  9. auto dateTime = localtime(&now);
  10. mut.lock(); // 为了防止打印错乱,我们在这里加锁
  11. cout << "任务编号:" << x <<" 执行线程ID: " << this_thread::get_id() << " 当前时间: " << dateTime->tm_min << ":" << dateTime->tm_sec << endl;
  12. mut.unlock();
  13. this_thread::sleep_for(2s);
  14. return x;
  15. }
  16. int main() {
  17. ThreadPool threadPool;
  18. for (int i = 0; i < 20; ++i) auto ret = threadPool.commit(func, i);
  19. this_thread::sleep_for(20s); // 主线程等待,因为现在子线程是脱离状态,如果主线程关闭,则看不到打印
  20. }

2.4 测试结果

可以看到我们的线程是并发执行的,总共用时从44分20秒,到44分26秒,总共6秒,加上我们最后一次打印没有停留2秒,总共是8秒,每次打印的线程号也相同,可以看出,我们实现了线程的复用

总结

这只是多线程的一个简单实现,很多东西没有考虑到,比如任务超时,任务优先级等,当然,我们会了简单的之后就能慢慢摸索更复杂的功能。感谢阅读

c++ 11 线程池---完全使用c++ 11新特性的更多相关文章

  1. 托管C++线程锁实现 c++11线程池

    托管C++线程锁实现   最近由于工作需要,开始写托管C++,由于C++11中的mutex,和future等类,托管C++不让调用(报错),所以自己实现了托管C++的线程锁. 该类可确保当一个线程位于 ...

  2. 简单的C++11线程池实现

    线程池的C++11简单实现,源代码来自Github上作者progschj,地址为:A simple C++11 Thread Pool implementation,具体博客可以参见Jakob's D ...

  3. C++ 11和C++98相比有哪些新特性

    此文是如下博文的翻译: https://herbsutter.com/elements-of-modern-c-style/ C++11标准提供了许多有用的新特性.这篇文章特别针对使C++11和C++ ...

  4. c++11 线程池学习笔记 (一) 任务队列

    学习内容来自一下地址 http://www.cnblogs.com/qicosmos/p/4772486.html github https://github.com/qicosmos/cosmos ...

  5. C++11线程池的实现

    什么是线程池 处理大量并发任务,一个请求一个线程来处理请求任务,大量的线程创建和销毁将过多的消耗系统资源,还增加了线程上下文切换开销. 线程池通过在系统中预先创建一定数量的线程,当任务请求到来时从线程 ...

  6. c++11线程池实现

    咳咳.c++11 增加了线程库,从此告别了标准库不支持并发的历史. 然而 c++ 对于多线程的支持还是比較低级,略微高级一点的使用方法都须要自己去实现,譬如线程池.信号量等. 线程池(thread p ...

  7. 基于C++11线程池

    1.包装线程对象 class task : public std::tr1::enable_shared_from_this<task> { public: task():exit_(fa ...

  8. 《java.util.concurrent 包源码阅读》11 线程池系列之ThreadPoolExecutor 第一部分

    先来看ThreadPoolExecutor的execute方法,这个方法能体现出一个Task被加入到线程池之后都发生了什么: public void execute(Runnable command) ...

  9. c++11线程池

    #pragma once #include <future> #include <vector> #include <atomic> #include <qu ...

随机推荐

  1. webbrowser 强制 ie11

    假设winform程序的名称是TestWebBrowser.exe. 1.在开始菜单内输入"regedit.exe",进入注册表编辑器 2.找到注册表项:HKEY_LOCAL_MA ...

  2. java的本地文件操作

    一.文件的创建.删除和重命名 File file = new File("/bin/hello.txt");//文件无法被创建,系统找不到指定的路径file.createNewFi ...

  3. Netty入门使用教程

    原创:转载需注明原创地址 https://www.cnblogs.com/fanerwei222/p/11827026.html 本文介绍Netty的使用, 结合我本人的一些理解和操作来快速的让初学者 ...

  4. 简单实现UITableView索引功能(中英文首字母索引) (二) By HL

    简单实现UITableView索引功能(中英文首字母索引)(一) ByH罗 相关类: NSString+PinYing(获取中英文首字母)   参考上面链接 #import "ViewCon ...

  5. MAC上安装HEAAN库

    介绍 HEAN是一个软件库,它实现支持定点运算的同态加密(HE),此库支持有理数之间的近似运算.近似误差取决于某些参数,与浮点运算误差几乎相同.该库中的方案发表在"近似数算术的同态加密&qu ...

  6. k8s 通过helm发布应用

    什么是helm? Helm 是 Kubernetes 的包管理器.Helm 是查找.分享和使用软件构建 Kubernetes 的最优方式. 在红帽系的Linux中我们使用yum来管理RPM包,类似的, ...

  7. hashlib模块&日志模块

    内容概要 hashlib模块 logging模块 第三方模块下载 内容详细 hashlib模块 hashlib 是一个提供了一些流行的hash(摘要)算法的Python标准库.其中所包括的算法有 md ...

  8. Note -「矩阵树定理」学习笔记

      大概--会很简洁吧 qwq. 矩阵树定理   对于无自环无向图 \(G=(V,E)\),令其度数矩阵 \(D\),邻接矩阵 \(A\),令该图的 \(\text{Kirchhoff}\) 矩阵 \ ...

  9. 【第二十四期】golang 一年经验开发 富途

    他们家是按题目来的,从一个小题目慢慢延伸着问,由浅入深,问到你换题为止. 第一题 给了一个网址,解释一下浏览器填入这个网址后发生了什么? TCP为什么要三次握手四次挥手? 502是什么? 如果出现50 ...

  10. VS Code开发TypeScript

    TypeScript是JaveScript的超集,为JavaScript增加了很多特性,它可以编译成纯JavaScript在浏览器上运行.TypeScript已经成为各种流行框架和前端应用开发的首选. ...