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

简单线程池实现

线程池的概念

线程池内部可以预先去进行创建出一批线程,对于每一个线程,它都会周期性的进行我们的任务处理。

线程内部在维护一个任务队列,其中我们外部可以向任务队列里放任务,然后内部的线程从任务队列里取任务,如果任务队列里没有任务时,所有线程全部去休眠,一旦外部生产出来对应的任务,那么就可以唤醒指定的一个或多个线程,让它进行任务的处理。

其实线程池也是一个典型的生产者消费者问题。

线程池是一种线程使用模式。

线程过多会带来调度开销,进而影响缓存局部和整体性能,而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。

线程池的优点

  • 线程池避免了在处理短时间任务时创建与销毁线程的代价。
  • 线程池不仅能够保证内核充分利用,还能防止过分调用。
  • 线程池当中线程的总数是固定的。也就是说,不管未来有多少个任务,线程的数量不会变化,而不是说我有任务就创建线程让线程去处理,这样任务越来越多,线程也会越来越多,这样的话对系统不友好。

注意: 线程池中可用线程的数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的应用场景

线程池常见的应用场景如下:

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应用户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。

相关解释:

  • 像Web服务器完成网页请求这样的任务,使用线程池技术使非常合适的。因为单个任务小,而任务数量大,你可以想象一个热门网站的点击次数。
  • 处于长时间的任务,比如TeInet连接请求,线程池的优点就不明显了。因为TeInet会话时间比线程的创建时间大多了。
  • 突发性大量客户请求,在没有线程池的情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,但短时间内产生大量线程可能使内存达到极限,出现错误。

线程池的实现

下面我们实现一个简单的线程池,线程池中提供了一个任务队列,以及若干个线程(多线程)。
在这里插入图片描述

  • 线程池中多个线程负责从任务队列中那任务,并将拿到的任务进行处理。
  • 线程池对外提供一个Push接口,用于让外部线程能够将任务Push到任务队列中。

设计逻辑

  1. 线程池初始化与启动:
    • 在初始化阶段,线程池会根据给定的线程数创建相应数量的线程,并存储在线程向量中。
    • 这些线程并不会立即启动,而是等到 Start 方法被调用时才会开始运行。
  2. 任务提交:
    • 使用 Push 方法向线程池提交任务。
    • 如果有线程正在等待任务,则会唤醒一个线程来处理新加入的任务。
  3. 线程间通信与同步:
    • 通过互斥锁 _mutex 和条件变量 _cond 实现对任务队列的保护以及线程间的同步。
    • 当队列为空时,工作线程会等待在条件变量上,直到有新的任务被加入或线程池停止运行。
  4. 线程池生命周期管理:
    • 通过 _isRuning 标志控制线程池的运行状态。
    • 通过 StartStop 方法控制线程池的启动和停止。
    • Wait 方法确保所有线程都完成了它们的任务之后才返回。

类结构

  • 模板参数: T 任务类型,是一个可以被调用的对象,如函数、lambda表达式或者实现 operator()的类。
  • 成员变量:
    • _threadNum:线程池中的线程数量。
    • _threads:保存线程池中所有线程的std::vector
    • _task_queue:保存待处理任务的std::queue
    • _mutex:保护_task_queue的互斥锁。
    • _cond:条件变量,用于线程间的同步。
    • _waitNum:当前等待在条件变量上的线程数。
    • _isRunning:标记线程池是否正在运行。

成员函数

  • 构造函数: 初始化线程数量(默认为全局变量 g_defaultThreadNum),互斥锁和条件变量。并生成对应的日志信息。
  • 析构函数: 销毁互斥锁和条件变量。 并生成对应的日志信息。
  • InitThreadPool 创建指定数量的工作线程,每个线程都有一个唯一的名称。线程创建后不会立即开始运行。每个线程通过 std::bind 绑定到 HandlerTask 方法上,并传入线程的名称(用于日志记录)。
  • Start 启动所有之前初始化的线程。这一步才是真正地创建线程。线程启动则代表线程池开始工作了, _isRunning应该被设置为true
  • Stop 线程池结束工作。设置 _isRunningfalse,唤醒所有还在等待的线程,通知线程退出循环。因为修改 _isRunning 会影响还在等待队列中的线程(共享区内部代码),所以最好在修改 _isRunning 前进行加锁。被唤醒的线程,将继续向下执行HandlerTask剩余逻辑。
  • Wait 等待所有线程完成它们的任务并结束。确保线程池完全停止运行。
  • Push 用于向任务队列中添加新任务。如果线程池正在运行,任务将被添加到队列中。如果有现成在等待队列上有任务可用,则唤醒其中一个线程。
  • HandlerTask 线程执行具体任务处理逻辑。它首先检查任务队列是否为空,如果队列为空且线程池仍在运行,则该线程将挂起等待;如果线程池停止运行并且队列为空,线程退出;如果队列中有任务,则取出并执行该任务。

这里为了简化代码,将之前封装的 Thread 中的模板去除,也去除该类中的 _data 属性,线程传入的函数参数则只需要传入 线程名 参数即可。同时因为 InitThreadPool 在创建线程时,通过 std::bind 将Thread对象绑定到 HandlerTask 方法上,这样不为这个对象传递执行任务参数,该对象也能执行 HandlerTask 方法。

// Thread.hpp
#ifndef __THREAD_HPP__
#define __THREAD_HPP__ #pragma once#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <unistd.h>
namespace ThreadModule
{using func_t = std::function<void(const std::string& name)>;class Thread {public:Thread(func_t func, const std::string name = "none-name"):_func(func), _threadName(name), _stop(true){}void Excute(){_func(_threadName);}static void* threadRoutine(void* args) {Thread* self = static_cast<Thread*>(args);self->Excute();return nullptr;}bool Start(){int n = pthread_create(&_tid, nullptr, threadRoutine, this);if(!n){_stop = false;return true;}else return false;}void Detach(){if(!_stop)pthread_detach(_tid);}void Join(){if(!_stop)pthread_join(_tid, nullptr);}const std::string& name(){return _threadName;}~Thread(){}private:pthread_t _tid;std::string _threadName;func_t _func;bool _stop;};
}#endif 

为什么线程池中需要有互斥锁和条件变量?

线程池中的任务队列是会被多个执行流同时访问的临界资源,因此我们需要引入互斥锁对任务队列进行保护。

线程池当中的线程要从任务队列里拿任务,前提条件是任务队列中必须要有任务,因此线程池当中的线程在拿任务之前,需要判断任务队列当中是否有任务,若此时队列为空,那么该线程应该进行等待,直到任务队列中有任务时再将其唤醒,因此我们需要引入条件变量。

注意:

  • 当某个线程被唤醒时,其可能是被异常或是伪唤醒,或者是一些广播类的唤醒线程操作而导致所有的线程被唤醒,使得在被唤醒的若干线程中,只有个别线程能拿到任务。此时应该让被唤醒的线程再次判断是否满足唤醒条件,所以在判断任务队列是否为空时,应该使用while进行判断,而不是if。
  • pthread_cond_broadcast函数的作用是唤醒条件变量下的所有线程,而外部可能只Push了一个任务,我们却把全部等待的线程都唤醒,此时这些线程就都会去任务队列获取任务,但最终只有一个线程能得到任务。一瞬间唤醒大量的线程可能会导致系统震荡,这叫做惊群效应。因此在唤醒线程时最好使用 pthread_cond_signal 函数唤醒正在等待的一个线程即可。
  • 当线程从任务队列中拿到任务后,该任务就已经属于当前线程了,与其他线程已经没有关系了,因此应该在解锁之后在进行处理任务,而不是在解锁之前进行。因为处理任务的过程可能会耗费一定的时间,所以我们不要将其放到临界区中。
  • 如果将处理任务的过程放到临界区当中,那么当某一线程从任务队列拿到任务后,其他线程还需要等待该线程将任务处理完后,才有机会进入临界区。此时虽然是线程池,但最终我们可能并没有让多线程并行的执行起来。

线程池源代码

#include <vector>
#include <queue>
#include <string>
#include <pthread.h>
#include "Log.hpp"
#include "Thread.hpp"
#include "Task.hpp"
using namespace ThreadModule;const static int g_defaultThreadNum = 3;template<typename T>
class ThreadPool
{
private:void LockQueue(){pthread_mutex_lock(&_mutex);}void UnlockQueue(){pthread_mutex_unlock(&_mutex);}void ThreadWakeup(){pthread_cond_signal(&_cond);}void ThreadWakeupAll(){pthread_cond_broadcast(&_cond);}void ThreadSleep(){pthread_cond_wait(&_cond, &_mutex);}public:ThreadPool(int threadNum = g_defaultThreadNum) : _threadNum(threadNum), _waitNum(0), _isRunning(false){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);LOG(INFO, "ThreadPool Construct()");}void HandlerTask(std::string name){LOG(INFO, "%s is running...", name.c_str());while(true) // 一直处理任务,直到队列中没有任务,退出{// 1. 访问任务队列,保证队列安全LockQueue();// 2. 判断队列中是否有数据// 2.1 如果队列为空,并且线程池是工作状态,则该线程等待while(_task_queue.empty() && _isRunning == true) {_waitNum++;ThreadSleep(); // 在条件变量上等待// 等待结束,继续判断,防止伪唤醒_waitNum--;}// 2.2 如果队列为空,并且线程池退出,则退出循环if(_task_queue.empty() && _isRunning == false){UnlockQueue();break;                }// 2.3 如果队列不为空,并且线程池退出,此时要先将队列中的任务完成,才能退出循环// 2.4 如果队列不为空,并且线程池未退出,取出任务,完成T t = _task_queue.front();_task_queue.pop();UnlockQueue();LOG(DEBUG, "%s, get a task", name.c_str());// 3. 执行任务t(); // 重载(), 也可以实现一个方法Run, t.Run()LOG(DEBUG, "%s has finished a task, result is %s", name.c_str(), t.ResultToString().c_str());}}void InitThreadPool(){for(int i = 0; i < _threadNum; i++){std::string name = "thread-" + std::to_string(i + 1);_threads.emplace_back(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1), name);LOG(INFO, "init thread %s done", name.c_str());}}void Start(){_isRunning = true;for(auto& thread: _threads){thread.Start();}}bool Push(const T& t){bool ret = false;LockQueue();if(_isRunning == true){_task_queue.push(t);if(_waitNum > 0)ThreadWakeup();LOG(DEBUG, "Push task success");ret = true;}UnlockQueue();return ret;}void Stop(){LockQueue();_isRunning = false;ThreadWakeupAll();UnlockQueue();}void Wait(){for(auto& thread:_threads){thread.Join();LOG(INFO, "%s is quit...", thread.name().c_str());}}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);LOG(INFO, "ThreadPool Destruct()");}
private:int _threadNum;std::vector<Thread> _threads;std::queue<T> _task_queue;pthread_mutex_t _mutex;pthread_cond_t _cond;int _waitNum;bool _isRunning;
};

其余详情源代码查看:源代码

结果展示:
在这里插入图片描述


今天的分享就到这里了,如果,你感觉这篇博客对你有帮助的话,就点个赞吧!感谢感谢……

相关文章:

简单线程池实现

线程池的概念 线程池内部可以预先去进行创建出一批线程&#xff0c;对于每一个线程&#xff0c;它都会周期性的进行我们的任务处理。 线程内部在维护一个任务队列&#xff0c;其中我们外部可以向任务队列里放任务&#xff0c;然后内部的线程从任务队列里取任务&#xff0c;如…...

CentOS7 安装 LLaMA-Factory

虚拟机尽量搞大 硬盘我配置了80G&#xff0c;内存20G 下载源码 git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git 如果下载不了&#xff0c;可以进入github手动下载&#xff0c;然后在传入服务器。 也可以去码云搜索后下载 安装conda CentOS7安装conda…...

最新扣子(Coze)案例教程:最新抖音视频文案提取方法替代方案,音频视频提取文案插件制作,手把手教学,完全免费教程

&#x1f468;‍&#x1f4bb; 星球群同学反馈&#xff0c;扣子平台的视频提取插件已下架&#xff0c;很多智能体及工作流不能使用&#xff0c;斜杠君这里研究了一个替代方案分享给大家。 方案原理&#xff1a;无论是任何视频或音频转文案&#xff0c;我们提取的方式首先都是要…...

三防笔记本有什么用 | 三防笔记本有什么特别

在现代社会&#xff0c;随着科技的不断进步&#xff0c;笔记本电脑已经成为人们工作和生活的重要工具。然而&#xff0c;在一些特殊的工作环境和极端条件下&#xff0c;普通笔记本电脑往往难以满足需求。这时&#xff0c;三防笔记本以其独特的设计和卓越的性能&#xff0c;成为…...

硬盘分区格式之GPT(GUID Partition Table)笔记250406

硬盘分区格式之GPT&#xff08;GUID Partition Table&#xff09;笔记250406 GPT&#xff08;GUID Partition Table&#xff09;硬盘分区格式详解 GPT&#xff08;GUID Partition Table&#xff09;是替代传统 MBR 的现代分区方案&#xff0c;专为 UEFI&#xff08;统一可扩展固…...

adb检测不到原来的设备List of devices attached解决办法

进设备管理器-通用串行总线设备 卸载无法检测到的设备驱动 重新拔插数据线...

案例分享(七):实现Apache-sharding-proxy的监控

案例分享(七):实现Apache-sharding-proxy的监控 背景部署流程背景 因业务需求,实现Apache-sharding-proxy的监控(基于Apache-sharding-agent)。 部署流程 1.下载agent的包,选择与sharding版本一致,要不然无法启动sharding 2.点击5.3.0之后可以看到有sharding,proxy…...

docker 安装 awvs15

安装好 docker bash <(curl -sLk https://www.fahai.org/aDisk/Awvs/check.sh) xrsec/awvs:v15等待完成后访问即可 地址: https://server_ip:3443/#/login UserName: awvsawvs.lan PassWord: Awvsawvs.lan修改密码 docker ps -a //查看容器&#xff0c;找到相应id d…...

Kafka在Vue和Spring Boot中的使用实例

Kafka在Vue和Spring Boot中的使用实例 一、项目概述 本项目演示了如何在Vue前端和Spring Boot后端中集成Kafka&#xff0c;实现实时消息的发送和接收&#xff0c;以及数据的实时展示。 后端实现&#xff1a;springboot配置、kafka配置、消息模型和仓库、消息服务和消费者、we…...

JSON 是什么?通俗详解

**JSON 是什么&#xff1f;通俗详解** --- ### **1. 一句话总结** **JSON&#xff08;JavaScript Object Notation&#xff09;** 是一种轻量级的 **数据交换格式**&#xff0c;就像“数据的快递包装盒”&#xff0c;用来在不同系统之间 **传递和存储信息**&#xff0c;简单易…...

Flutter:Flutter SDK版本控制,fvm安装使用

1、首先已经安装了Dart&#xff0c;cmd中执行 dart pub global activate fvm2、windows配置系统环境变量 fvm --version3、查看本地已安装的 Flutter 版本 fvm releases4、验证当前使用的 Flutter 版本&#xff1a; fvm flutter --version5、切换到特定版本的 Flutter fvm use …...

碰一碰发视频源头开发技术服务商

碰一碰发视频系统 随着短视频平台的兴起&#xff0c;用户的创作与分享需求日益增长。而如何让视频分享更加便捷、有趣&#xff0c;则成为各大平台优化的重点方向之一。抖音作为国内领先的短视频平台&#xff0c;在2023年推出了“碰一碰”功能&#xff0c;通过近距离通信技术实…...

Oracle 23ai Vector Search 系列之4 VECTOR数据类型和基本操作

文章目录 Oracle 23ai Vector Search 系列之4 VECTOR数据类型和基本操作VECTOR 数据类型基本语法Vector 维度限制和向量大小向量存储格式&#xff08;DENSE vs SPARSE&#xff09;1. DENSE存储2. SPARSE存储3. 内部存储与空间计算 Oracle VECTOR数据类型的声明格式VECTOR基本操…...

Java面试38-Dubbo是如何动态感知服务下线的?

首先&#xff0c;Dubbo默认采用Zookeeper实现服务注册与服务发现&#xff0c;就是多个Dubbo服务之间的通信地址&#xff0c;是使用Zookeeper来维护的。在Zookeeper上&#xff0c;会采用树形结构的方式来维护Dubbo服务提供端的协议地址&#xff0c;Dubbo服务消费端会从Zookeeper…...

C++day8

思维导图 牛客练习 练习 #include <iostream> #include <cstring> #include <cstdlib> #include <unistd.h> #include <sstream> #include <vector> #include <memory> using namespace std; class user{ public: …...

MySQL的进阶语法8(SQL优化——insert、主键、order by、group by、limit、count和update)

目录 一、插入数据 1.1 insert 1.2 大批量插入数据 二、主键优化 2.1 数据组织方式 2.2 页分裂 2.2.1 主键顺序插入效果 2.2.2 主键乱序插入效果 2.3 页合并 2.4 索引设计原则 三、order by优化 3.1 执行以下两条语句&#xff08;无索引&#xff09; 3.2 创建索引…...

STM32 基础2

STM32中断响应过程 1、中断源发出中断请求。 2、判断处理器是否允许中断&#xff0c;以及该中断源是否被屏蔽。 3、中断优先级排队。 4、处理器暂停当前程序&#xff0c;保护断点地址和处理器的当前状态&#xff0c;根据中断类型号&#xff0c;查找中断向量表&#xff0c;转到…...

前端单页应用性能优化全指南:从加载提速到极致体验

一、SPA性能瓶颈深度剖析 1.1 核心性能指标解读 指标健康阈值测量工具优化方向FCP (首次内容渲染)< 1.8sLighthouse资源加载优化TTI (可交互时间)< 3.5sWebPageTestJavaScript优化LCP (最大内容渲染)< 2.5sChrome DevTools渲染性能优化CLS (布局偏移)< 0.1PageSp…...

自然语言处理利器NLTK:从入门到核心功能解析

文章目录 一、NLP领域的基石工具包二、NLTK核心模块全景解析1 数据获取与预处理2 语言特征发现3 语义与推理 三、设计哲学与架构优势1 四维设计原则2 性能优化策略 四、典型应用场景1 学术研究2 工业实践 五、生态系统与未来演进 一、NLP领域的基石工具包 自然语言工具包&…...

简述Unity对多线程的支持限制和注意事项

Unity是一个以单线程为核心设计的游戏引擎&#xff0c;其主线程负责渲染、物理模拟、脚本更新&#xff08;如Update和FixedUpdate&#xff09;等核心功能。虽然Unity允许开发者使用C#的多线程功能&#xff08;如System.Threading命名空间&#xff09;来创建和管理线程&#xff…...

Mysql 中有哪些日志结构?

在 MySQL 中&#xff0c;日志文件是非常重要的&#xff0c;它们用于记录数据库的各类活动&#xff0c;帮助管理员进行监控、调试、恢复、以及优化数据库性能。MySQL 提供了几种类型的日志&#xff0c;每种日志都有其特定的用途。以下是 MySQL 中常见的几种日志类型&#xff1a;…...

【第2月 day17】Matplotlib 新手设计的直方图与饼图学习内容

以下是专为Python新手设计的直方图与饼图学习内容&#xff0c;包含基础知识、代码演示及注意事项&#xff1a; 一、直方图&#xff08;Histogram&#xff09; 1. 直方图的作用 展示数据分布情况&#xff08;如年龄分布、成绩分布&#xff09;观察数据集中趋势、离散程度 2. …...

使用Docker安装及使用最新版本的Jenkins

1. 拉取镜像 通过Windows powerShell执行命令行&#xff08;2选1&#xff09;&#xff1a; -- 长期支持版 docker pull jenkins/jenkins:lts-- 最新版 docker pull jenkins/jenkins:latest 2. 创建并执行容器 你可以通过以下命令来运行Jenkins容器&#xff0c;执行命令&…...

在Spring Boot中配置数据库连接

今天我们要谈谈如何在Spring Boot项目中配置数据库连接。我们会创建两个Java类&#xff1a;DatabaseProperties.java和DataSourceConfig.java&#xff0c;并在我们的应用程序中注入这些配置。让我们一起乘风破浪&#xff0c;开始这段编码之旅吧&#xff01; 目录 创建DatabaseP…...

Tiktok 关键字 视频及评论信息爬虫(2) [2025.04.07]

&#x1f64b;‍♀️Tiktok APP的基于关键字检索的视频及评论信息爬虫共分为两期&#xff0c;希望对大家有所帮助。 第一期&#xff1a;基于关键字检索的视频信息爬取 第二期见下文。 1.Node.js环境配置 首先配置 JavaScript 运行环境&#xff08;如 Node.js&#xff09;&…...

关于深度学习中内部协变量偏移问题小记

内部协变量偏移问题 内部协变量偏移&#xff08;Internal Covariate Shift&#xff0c;简称ICS&#xff09;是深度学习中一个重要的概念&#xff0c;用来描述神经网络在训练过程中&#xff0c;各层输入分布发生变化的现象。这种分布偏移会导致训练不稳定、收敛变慢甚至失败。2…...

15-产品经理-维护需求

一、提研发需求 在产品–研发需求列表页&#xff0c;点击“提研发需求”按钮&#xff0c; 在提研发需求页面&#xff0c;可以选择已有的计划。也可以在计划页面里进行关联。 未编辑完的需求可以点击【存为草稿】按钮&#xff0c;保存为草稿状态&#xff0c;待编辑完成再选择提…...

JVM基础架构:内存模型×Class文件结构×核心原理剖析

&#x1f680;前言 “为什么你的Java程序总在半夜OOM崩溃&#xff1f;为什么某些代码性能突然下降&#xff1f;一切问题的答案都在JVM里&#xff01; 作为Java开发者&#xff0c;如果你&#xff1a; 对OutOfMemoryError束手无策看不懂GC日志里的神秘数字好奇.class文件如何变…...

js前端对时间进行格式处理

时间格式处理 通过js前端&#xff0c;使用dayjs库进行格式化 安装dayjs库 npm install dayjs 封装成日期格式化工具类 formatter.ts // 导入 dayjs&#xff0c;先安装依赖 npm install dayjs import dayjs from "dayjs"; import utc from "dayjs/plugin/utc…...

如何拿到iframe中嵌入的游戏数据

在 iframe 中嵌入的游戏数据是否能被获取&#xff0c;取决于以下几个关键因素&#xff1a; 1. 同源策略 浏览器的同源策略是核心限制。如果父页面和 iframe 中的内容同源&#xff08;即协议、域名和端口号完全相同&#xff09;&#xff0c;那么可以直接通过 JavaScript 访问 …...