当前位置: 首页 > news >正文

线程池学习(一)

一、线程池

1、线程与进程的区别?

进程是操作系统分配资源的基本单位,线程是操作系统进行CPU调度的单位。

2、线程池的好处?

复用线程资源,减少创建和销毁线程的开销。

3、线程池是什么?

维持管理一定数量线程的池式结构。

  • 维持:复用资源
  • 管理:线程是参与CPU调度的,如果暂时不需要线程执行,就将其进行休眠,能最大限度的使用资源。
  • 为什么是一定数量,而不是固定数量?:操作系统运行线程是通过CPU的核心去运行的,而CPU的核心数量是有限的,如果线程池中的线程数过多,超过了核心的数量,那么会带来线程切换的开销,反而降低了效率。

4、线程池解决什么问题?

  • 异步执行耗时任务,不过度占用核心线程资源(生产者线程)
    • 耗时:耗时等待(io),耗时计算。
  • 充分利用多核

5、线程池是如何解决问题的?

生产消费模型角度来看:

  • 生产者:核心线程,抛出耗时任务到队列,并唤醒一个休眠线程。
  • 消费者:线程池线程,从队列中取出任务执行;当队列为空时,线程进入休眠状态。
    在这里插入图片描述

二、具体实现

1、面向生产者线程

发布任务到线程池。

void ThreadPoll::Post(std::function<void()> task)
{m_queuePlus->Push(task);
}

2、任务队列

std::queue<std::function<void()>> m_queue;     //任务队列
//但该队列属于临界资源,不利于多线程安全,需要加互斥锁,读写锁等来保证线程安全。

使用阻塞队列,将锁封装在BlockQueue中

BlockQueue<std::function<void()>> m_queue;     //任务队列

3、维持管理一定数量线程

std::vector<std::thread> m_workthreads;     //线程集合

4、线程池线程数量的确定

  • 耗时计算任务:核心线程数 = CPU核心数,因为计算任务是CPU密集型,不会涉及到用户态和内核态的切换,全是用户态执行,所以核心线程数要和CPU的核心数一致。不过这个值是理论值,实际上,需要在该值的基础上,+1,+2,-1,-2等去测试,找到最优值。(测试单位时间内执行任务数来决定)

  • 耗时等待任务:IO密集型任务,IO在Linux下主要分为文件IO和网络IO,因为涉及到用户态和内核态的切换以及IO的等待,所以核心线程数 = 2 * CPU核心数,让一部分线程在等待IO时,能有更多的线程去执行其他任务。同理,也需要测试找到最优值。(测试单位时间内执行任务数来决定)

  • (线程等待时间 + cpu运算时间) * cpu核心线程数 / cpu运算时间 = 线程池最优核心数

5、核心线程抛出任务到队列,并唤醒一个休眠线程

void Push(const T &value) // 入队操作
{std::lock_guard<std::mutex> lock(m_mutex);m_queue.push(value);m_cond.notify_one(); // 唤醒一个等待的线程
}

6、线程池线程从队列中取出任务执行;当队列为空时,线程进入休眠状态。

void ThreadPoll::Worker()
{while (true){std::function<void()> task;if (!m_queuePlus->Pop(task))break;task();}
}bool Pop(T &value) // 出队操作,考虑正常情况与异常情况(队列为空)
{std::unique_lock<std::mutex> lock(m_mutex); // 与lock_guard相比,unique_lock可以延迟锁定,可手动调用unlock,并且同样在析构时自动释放锁。m_cond.wait(lock, [this]{ return !m_queue.empty() || m_nonblock; }); // 若队列为空,则阻塞等待,直到队列不为空或者m_nonblock为true时才继续执行。if (m_queue.empty())return false;value = m_queue.front();m_queue.pop();return true;
}

7、执行完毕,销毁线程

ThreadPoll::~ThreadPoll()
{m_queuePlus->CancelBlock();for (auto &thread : m_workthreads){if (thread.joinable())thread.join();}
}

8、结果:

在这里插入图片描述

9、优化

之前生产者和消费者共用一个队列,并且用一个互斥锁,这样会导致生产者线程和消费者线程都在等待同一个锁;线程数量少,以及任务量少时,感觉不出来,当线程数量变多,任务量多时,效率就会变得低下。

  • 优化方案:生产者对应生产者队列,消费者对应消费者队列。
//在之前阻塞队列里面封装了两个队列,一个生产者队列和一个消费者队列,两把互斥锁,一个生产者队列对应一把锁,一个消费者队列对应一把锁。
std::queue<T> prod_queue_;
std::queue<T> cons_queue_;
std::mutex prod_mutex_;
std::mutex cons_mutex_;int SwapQueue()
{std::unique_lock<std::mutex> lock(m_prod_mutex); // 与lock_guard相比,unique_lock可以延迟锁定,可手动调用unlock,并且同样在析构时自动释放锁。m_cond.wait(lock, [this]{return !m_prod_queue.empty() || m_nonblock; }); // 若队列为空,则阻塞等待,直到队列不为空或者m_nonblock为true时才继续执行。std::swap(m_prod_queue, m_cons_queue);return m_cons_queue.size();
}

在这里插入图片描述

三、总结:

  1. 进程与线程的关系

    • 进程作为操作系统资源分配的基本单位
    • 线程作为CPU调度的基本执行单元
  2. 线程池的核心优势

    • 实现线程资源的高效复用
    • 显著降低频繁创建和销毁线程的系统开销
  3. 线程池的设计原则

    • 采用池化技术管理固定数量的线程
    • 遵循"适量最优"原则,而非盲目增加线程数量
  4. 线程池的核心价值

    • 充分发挥多核CPU的计算优势
    • 实现主线程与耗时任务的异步解耦执行

四、问题:

1、为什么使用队列?

队列是一种双端口操作的数据结构。

  • 职责:生产者对应一个端口,消费者对应另一个端口。
  • 操作队列的时间复杂度为O(1),而vector为O(n),后续加锁灵活。

代码链接:
Code
0voice·Github

相关文章:

  • 小白成长之路-Linux操作系统-进程管理
  • JVM——Truffle:语言实现框架
  • JVM与JMM深度解析:从Java 8到Java 21的演进
  • 字符串day8
  • Python 如何让自动驾驶的“眼睛”和“大脑”真正融合?——传感器数据融合的关键技术解析
  • 弧度 = 弧长与半径的比值
  • Linux系统之cal命令的基本使用
  • QT6 关于使用MSVC2019,UI设计师自定义控件的制作和QT Cretor里面调用
  • 多模态简介
  • QTest应用迷城
  • 命象架构法 02|你的系统有“用神”吗?
  • Opera Neon发布该公司首款“AI代理”浏览器
  • 蛋白质研究用蛋白酶购买渠道推荐
  • 智汇云舟携最新无人机2D地图快速重建技术亮相广西国际矿业展览会
  • Java+POI+EXCEL导出柱形图(多列和单列柱形图)
  • 极坐标下 微小扇环 面积微元
  • 虚拟与现实时空认知同步的核心指标
  • 鸿蒙完整项目-仿盒马App(一)首页静态页面
  • GoldenDB管理节点zk部署
  • 华南版权服务大厅启用:富唯智能携具身智能人形机器人亮相,赋能版权产业生态革新
  • 手机网站优化/打开百度一下你就知道
  • 上海做网站吧/站长工具浪潮
  • 阳江做网站seo/百度云搜索引擎入口
  • 斗鱼网站的实时视频是怎么做的/优化seo设置
  • 漳州 外贸网站建设 SEO/全自动推广引流软件
  • 街道口做网站公司/优化搜索引擎的方法