C++线程池的原理(画图)及简单实现+例子(加深理解)

news2024/11/26 22:45:05

1.为什么线程池会出现,解决什么问题?

C++线程池(ThreadPool)的出现主要是为了解决以下几个问题:
1.性能:创建和销毁线程都是相对昂贵的操作,特别是在高并发场景下,频繁地创建和销毁线程会极大地降低程序的性能。通过线程池预先创建一定数量的线程并保存在内存中,可以避免频繁地创建和销毁线程,从而提高程序的性能。
2.资源管理:线程是操作系统级别的资源,如果线程数量过多,可能会导致系统资源的过度消耗,甚至可能导致系统崩溃。通过线程池,可以控制同时运行的线程数量,避免资源过度消耗。
3.任务调度:线程池可以更方便地进行任务的调度。通过线程池,可以将任务分配给不同的线程执行,实现并行处理,提高程序的执行效率。
4.简化编程:使用线程池可以简化多线程编程的复杂性。程序员只需要将任务提交给线程池,而不需要关心线程的创建、管理和销毁等细节,降低了多线程编程的难度。
因此,C++线程池的出现是为了解决在高并发场景下创建和销毁线程的开销问题,提高程序的性能和并发处理能力,简化多线程编程的复杂性

................

2.简单解释下原理

线程池初始化时,初始化线程,也可以生成一个管理线程,来管理工作线线程数量。

如果当前任务队列一直有很多任务时,说明线程繁忙,处理不过来,可以根据设置的最大工作线程数来新增线程,提高并发处理能力,提高工作效率。

如果当前任务队列一直为空,说明当前时间段,没有任务或者很少的任务要处理,可以销毁多余的空闲线程,避免资源浪费。

如下图所示:

初始化线程池后,有新任务到来后,线程池处理流程为:

将新任务投递到线程队列中->发送信号通知线程处理->空闲线程处理->处理完成检查任务队列是否还有任务->有任务则提取任务处理,没有任务就挂起为空闲线程,避免占用系统资源

如下图所示:

3. 实现

用一个Task类表示任务,成员函数如下

    void (*hander)(void* arg); // 要执行任务的函数指针
    void *arg;          //可执行任务的参数
    std::string name;   //任务名
    static int taskNum; //任务数量
// task.h
class Task
{
public:
    Task();
    Task(std::string name);
    Task(void *arg);
    void createTask(void (*hander)(void* arg), void*arg);
    ~Task();

    void (*hander)(void* arg);
    void *arg;
    //Task* next;
    std::string name;
    static int taskNum;
};

// task.cpp
int Task::taskNum = 0;

Task::Task()
{
    next = nullptr;
    this->arg = nullptr;
    taskNum++;
}

Task::Task(std::string name)
{
    this->name = name;
    taskNum++;
}

Task::Task(void *arg)
{
    next = nullptr;
    this->arg = arg;
    taskNum++;
}

void Task::createTask(void (*hander)(void *), void *arg)
{
    this->hander = hander;
    this->arg = arg;
}

Task::~Task()
{
    if(--taskNum==0) {
        qDebug()<<"所有任务执行完成, 完成时间:"<<QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss:zzz");
        free(this->arg);
    }
}

自定义线程池如下:

typedef struct {   
   Task* first;  //任务队列头  
   Task** last;  //任务队列尾部
}threadPool_Queue;

threadPool_Queue 这个结构提用来表示任务队列,对指针特性用的很巧妙,但是不好理解,我就不讲这个了,我在代码中注释掉了,感兴趣的可以把注释放开,理解下。现在用stl中的queue更好理解,用到如下成员函数,由于为了简便理解线程池,我就把管理线程去掉了

    std::queue<Task*> taskQueue;  // 任务队列
    std::condition_variable cond; / /条件变量,用于唤醒线程和挂起线程
    std::mutex mutexPool;         // 线程互斥锁,保护线程安全
    unsigned long m_maxNum;       // 线程最大数量
    unsigned long m_minNum;       // 线程最小数量
    int busyNum;                  // 线程是否繁忙,用于管理线程
    int aliveNum;                 // 活跃线程数,即繁忙线程数
    std::vector<std::thread> m_threads;// 线程队列

用到两个函数:

    void taskPost(Task* task);    //投递任务到任务队列中  
    static void worker(void* arg);//工作线程函数,用来循环不断的处理任务

代码如下所示:

// threadpool.h
#include <thread>
#include <vector>
#include <condition_variable>
#include <mutex>
#include <queue>
#include "task.h"

typedef struct {
    Task* first;
    Task** last;
}threadPool_Queue;

class ThreadPool
{

public:
    ThreadPool(unsigned long maxNum);
    ~ThreadPool();
    void taskPost(Task* task);

    static void worker(void* arg);

private:
    //threadPool_Queue queue;
    std::queue<Task*> taskQueue;
    std::condition_variable cond;
    std::mutex mutexPool;
    unsigned long m_maxNum;
    unsigned long m_minNum;
    int busyNum;
    int aliveNum;
    std::vector<std::thread> m_threads;
};

// threadpool.cpp
#include "threadpool.h"
#include "task.h"
#include <QDebug>
#include <iostream>
#include <QDateTime>

ThreadPool::ThreadPool(unsigned long maxNum)
{
    m_maxNum = maxNum;
    m_minNum = 1;

    busyNum = 0;
    aliveNum = 0;
    //queue.first = nullptr;
    //queue.last = &queue.first;
    m_threads.resize(maxNum);
    for(unsigned long i=0; i<maxNum;i++) {
        m_threads[i] = std::thread(worker, this);
    }
}

ThreadPool::~ThreadPool()
{
    //唤醒阻塞的工作线程
    cond.notify_all();
    for (unsigned long i = 0; i < m_maxNum; ++i)
    {
        if (m_threads[i].joinable()) {
            m_threads[i].join();
        }
    }
}

void ThreadPool::taskPost(Task *task)
{
    std::unique_lock<std::mutex> lk(mutexPool);
    //task->next = nullptr;
    //*queue.last = task;
    //queue.last = &task->next;
    taskQueue.push(task);
    // 通知线程处理
    cond.notify_one();
    lk.unlock();
}

void ThreadPool::worker(void *arg)
{

    ThreadPool* pool = static_cast<ThreadPool*>(arg);

    while (1) {
        // unique_lock在构造时或者构造后(std::defer_lock)获取锁
        std::unique_lock<std::mutex> lk(pool->mutexPool);
        //while (!pool->queue.first) { //没有任务时,线程挂起
        while (pool->taskQueue.empty()) { //没有任务时,线程挂起
            //挂起,直到收到主线程的事件通知
            pool->cond.wait(lk);
        }
        /*Task* task = pool->queue.first;
        pool->queue.first = task->next;
        lk.unlock();
        if(pool->queue.first==nullptr )  {
            pool->queue.last=&pool->queue.first;
        }*/
        Task* task = pool->taskQueue.front();
        pool->taskQueue.pop();//从队列中移除
        // 当访问完线程池队列时,线程池解锁
        lk.unlock();
        qDebug()<<"执行任务任务开始时间:"<<QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss:zzz");

        task->hander(task->arg);
        std::cout << task->name << " finish!" <<std::endl;
        delete task;
    }
}

4. 单线程与线程池的对比结果

然后在写个测试的例子,突出线程池的并发能力

先定义一个任务函数,用来计算累加值,计算1+2+3+...+value的值,相当于一个任务

void executeTask_1(void* size) {
    int* s = static_cast<int*>(size);
    int value = *s;
    long sum=0;
    for(int i=0; i<value; i++) {
        sum+=i;
    }
    std::cout << "计算完成,sum= " <<sum << std::endl;
}

然后在main函数中,执行四次这个任务,用一般流程下(即单线程)执行四次这个任务需要多长时间,和在线程池中的执行时间进行对比,我在线程池中初始化了四个线程,用于并发处理任务

int main()
{
    long* n = new long;
    *n = 1000000000;
    // 获取当前时间
    auto start = std::chrono::system_clock::now();
    executeTask_1(n);
    executeTask_1(n);
    executeTask_1(n);
    executeTask_1(n);
    // 获取操作完成后的时间
    auto end = std::chrono::system_clock::now();
    // 计算时间差
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    // 输出时间差(以毫秒为单位)
    std::cout << "一般流程执行所有任务所需时间: " << duration.count() << "毫秒" << std::endl;


    ThreadPool pool(4);
    Task* t1 = new Task("Task1");
    t1->createTask(executeTask_1, n);
    Task* t2 = new Task("Task2");
    t2->createTask(executeTask_1, n);
    Task* t3 = new Task("Task3");
    t3->createTask(executeTask_1, n);
    Task* t4 = new Task("Task4");
    t4->createTask(executeTask_1, n);
    // 获取当前时间
    qDebug()<<"线程池执行任务开始时间:"<<QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss:zzz");
    pool.taskPost(t1);
    pool.taskPost(t2);
    pool.taskPost(t3);
    pool.taskPost(t4);

    return 0;
}

结果如下所示:

可以看出,计算四次从1-十亿累加值的任务一般流程(即单线程)需要7.816秒,平均每一次:1.954秒

而用线程池执行的任务是同时开始的(在毫秒误差内),所有任务执行完成,用了41.131-39.117=2.014秒

数据对比可以看出,使用4个线程并发处理,和一个线程处理的时间差不多,说明线程池的并发处理是没有问题的

我把线程池线程数初始化为2,也符合预判

5. 总结

可以看出,在处理多个相同任务的时候,线程池(线程数量为4时)的速度几乎是单线程的4倍,当然,线程数不是越多越好,取决于CPU的核数(最好不要大于CPU的核数,因为太多的工作线程会竞争CPU的资源,带来不必要的上下文切换,小于CPU的核数则不能够充分利用CPU)。

对于某些场景,使用线程池是很有必要的,在需要高并发的服务器中线程池几乎是必备的,如文件传输的服务器,多用户下载或者上传文件时,几乎是同步的,在高并发场景下,如果每个请求都创建一个新线程,会导致线程数量过多,同时线程的创建和销毁也需要消耗大量的资源。为了解决这些问题,可以使用线程池技术。线程池预先创建一定数量的线程,并且可以在多个请求之间复用这些线程,从而提高服务器的处理能力和资源利用率。因此,在高并发的服务器中,使用线程池技术可以有效地降低资源消耗、提高系统性能和响应速度,是非常必要和常用的技术手段之一。

虽然现在有很多封装好的线程池供我们调用。但是,其原理也是值得我们推敲的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1354405.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux下误删除后的恢复操作测试之extundelete工具使用

一、工具介绍 extundelete命令的功能可用于系统删除文件的恢复。在使用前&#xff0c;需要先将要恢复的分区卸载&#xff0c;以防数据被意外覆盖。 语法格式&#xff1a;extundelete [参数] 文件或目录名 常用参数&#xff1a; --after 只恢复指定时间后被删除的文件 --bef…

Linux学习(9)——RAID与服务器的常见故障

目录 一、服务器常见故障 1、系统不停重启进入不了系统 2、卡在开机界面右下角有fA B2 H8 3、系统安装不上 4、如何进入服务器的bios 5、一般进入阵列卡的快捷键 6.网络不通 7.硬盘不识别 二、RAID相关知识 1、RAID的概念 2、RAID功能实现 3、RAID实现的方式 三、…

机器学习笔记 - 偏最小二乘回归 (PLSR)

一、偏最小二乘回归:简介 PLS 方法构成了一个非常大的方法族。虽然回归方法可能是最流行的 PLS 技术,但它绝不是唯一的一种。即使在 PLSR 中,也有多种不同的算法可以获得解决方案。PLS 回归主要由斯堪的纳维亚化学计量学家 Svante Wold 和 Harald Martens 在 20 世纪 80 年代…

海外服务器2核2G/4G/8G和4核8G配置16M公网带宽优惠价格表

腾讯云海外服务器租用优惠价格表&#xff0c;2核2G10M带宽、2核4G12M、2核8G14M、4核8G16M配置可选&#xff0c;可以选择Linux操作系统或Linux系统&#xff0c;相比较Linux服务器价格要更优惠一些&#xff0c;腾讯云服务器网txyfwq.com分享腾讯云国外服务器租用配置报价&#x…

ByteTrack算法流程的简单示例

ByteTrack ByteTrack算法是将t帧检测出来的检测框集合 D t {\mathcal{D}_{t}} Dt​ 和t-1帧预测轨迹集合 T ~ t − 1 {\tilde{T}_{t-1}} T~t−1​ 进行匹配关联得到t帧的轨迹集合 T t {T_{t}} Tt​。 首先使用检测器检测t帧的图像得到检测框集合 D t {\mathcal{D}_{t}} …

手机技巧:分享10个vivo手机实用小技巧技巧,值得收藏

目录 1. 快速切换应用 2、智能助手Jovi 3. 轻按唤醒屏幕 4. 快速启动相机 5. 分屏功能 6. 手势操作 7. 一键清理 8.忘记密码 9.玩游戏耗电快 10.手机丢失后该怎么办 1. 快速切换应用 向右或向左滑动底部的虚拟按键即可。 2、智能助手Jovi vivo手机自带智能助手Jovi…

【Java EE初阶八】多线程案例(计时器模型)

1. java标准库的计时器 1.1 关于计时器 计时器类似闹钟&#xff0c;有定时的功能&#xff0c;其主要是到时间就会执行某一操作&#xff0c;即可以指定时间&#xff0c;去执行某一逻辑&#xff08;某一代码&#xff09;。 1.2 计时器的简单介绍 在java标准库中&#xff0c;提供…

CMake入门教程【核心篇】添加应用程序(add_executable)

&#x1f608;「CSDN主页」&#xff1a;传送门 &#x1f608;「Bilibil首页」&#xff1a;传送门 &#x1f608;「本文的内容」&#xff1a;CMake入门教程 &#x1f608;「动动你的小手」&#xff1a;点赞&#x1f44d;收藏⭐️评论&#x1f4dd; 文章目录 1. 概述2. 使用方法2…

【计算机视觉】常用图像数据集

图像数据集 模型需要好的数据才能训练出结果&#xff0c;本文总结了机器学习图像方面常用数据集。 MNIST 机器学习入门的标准数据集&#xff08;Hello World!&#xff09;&#xff0c;10个类别&#xff0c;0-9 手写数字。包含了60,000 张 28x28 的二值训练图像&#xff0c;10…

计算机网络(2)

计算机网络&#xff08;2&#xff09; 小程一言专栏链接: [link](http://t.csdnimg.cn/ZUTXU) 计算机网络和因特网&#xff08;2&#xff09;分组交换网中的时延、丢包和吞吐量时延丢包吞吐量总结 协议层次及其服务模型模型类型OSI模型分析TCP/IP模型分析 追溯历史 小程一言 我…

Graphics Control

Graphics Control提供了一个易于使用的图形设置管理解决方案,帮助您加快开发。它附带了一个常用设置库,如分辨率、垂直同步、全屏模式、光晕、颗粒、环境光遮挡等。我们的可自定义设置面板UI预制件为您提供了一个可用的UI面板,支持完整的游戏手柄和键盘输入。图形控制还附带…

【前沿技术杂谈:ChatGPT】ChatGPT——热潮背后的反思

【前沿技术杂谈&#xff1a;ChatGPT】ChatGPT——热潮背后的反思 缘起&#xff1a;无中生有&#xff0c;涅槃重生人工智能技术人工智能的发展史无中生有内容自动生成技术的发展代表企业OpenAI-GPT系列技术的发展历程ChatGPT新特点 热潮&#xff1a;万众瞩目&#xff0c;群雄逐鹿…

Unity | Shader基础知识番外(向量数学知识速成)

目录 一、向量定义 二、计算向量 三、向量的加法&#xff08;连续行走&#xff09; 四、向量的长度 五、单位向量 六、向量的点积 1 计算 2 作用 七、向量的叉乘 1 承上启下 2 叉乘结论 3 叉乘的计算&#xff08;这里看不懂就百度叉乘计算&#xff09; 八、欢迎收…

Vue3地图选点组件

Vue3地图选点组件 <template><div style"width: 100%; height: 500px"><div class"search-container"><el-autocompletev-model"suggestionKeyWord"class"search-container__input"clearable:fetch-suggestion…

【已解决】You have an error in your SQL syntax

报错讯息 java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘desc,target_url,sort,status,create_by,modify_by,created,last_update_time FROM…

图像分割 分水岭法 watershed

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 本文的C#版本请访问&#xff1a;图像分割 分水岭法 watershed&#xff08;C#&#xff09;-CSDN博客 Watershed算法是一种图像处理算…

SSM的校园二手交易平台----计算机毕业设计

项目介绍 本次设计的是一个校园二手交易平台&#xff08;C2C&#xff09;&#xff0c;C2C指个人与个人之间的电子商务&#xff0c;买家可以查看所有卖家发布的商品&#xff0c;并且根据分类进行商品过滤&#xff0c;也可以根据站内搜索引擎进行商品的查询&#xff0c;并且与卖…

XCTF-Misc1 USB键盘流量分析

m0_01 附件是一个USB流量文件 分析 1.键盘流量 USB协议数据部分在Leftover Capture Data域中&#xff0c;数据长度为八个字节&#xff0c;其中键盘击健信息集中在第三个字节中。 usb keyboard映射表&#xff1a;USB协议中HID设备描述符以及键盘按键值对应编码表 2.USB…

苹果手机数据删除怎么恢复?这几个方法值得一试!

不小心删除了iPhone里的照片&#xff1f;别担心&#xff0c;数据恢复是有可能的&#xff01; 从这里&#xff0c;你可以找到你的备份并恢复丢失的数据。如果你没有备份&#xff0c;那么数据恢复软件可能可以帮助你。它们可以扫描你的iPhone或iTunes备份&#xff0c;找到你删除…