基于C++11 实现的线程池

news2025/2/3 19:48:49

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

基于C++11 实现的线程池

    • 基于C++11 实现的线程池
      • 1、线程池原理
      • 2、线程池的设计思路?
      • 3、一个基于C++11的优秀的线程池
        • 3.1 头文件
        • 3.2 线程池类
        • 3.3 构造函数实现
        • 3.4 入队—添加新的工作任务到线程池
        • 3.5 析构函数
        • 3.6 测试代码
        • 3.7 演示


基于C++11 实现的线程池

  C++11加入了线程库,这是历史性的一步跨越,因为它已然能够实现简单的并发了,但有这样一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

  如何解决这种线程的频繁创建销毁呢?就是线程执行完一个任务,不会被销毁,而是可以继续执行其他的任务呢?

  一个好的办法就是使用线程池,不过线程池是个什么东西呢?它又该怎么实现呢?

1、线程池原理

  线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件), 则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

2、线程池的设计思路?

实现线程池有以下几个步骤:

(1)设置一个生产者消费者队列,作为临界资源。

(2)初始化n个线程,并让其运行起来,加锁去队列里取任务运行

(3)当任务队列为空时,所有线程阻塞。

(4)当生产者队列来了一个任务后,先对队列加锁,把任务挂到队列上,然后使用条件变量去通知阻塞中的一个线程来处理。

3、一个基于C++11的优秀的线程池

  • 此线程池来源于github上一个6.1k star的线程池项目:
  • 源码地址:https://github.com/progschj/ThreadPool

3.1 头文件

#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex> // 锁
#include <condition_variable> // 条件变量
#include <future>
#include <functional>
#include <stdexcept>

  头文件中,没有什么特别的,这里稍微说下:thread—线程相关的库,mutex—互斥量,也就是互斥锁,condition_variable—条件变量,用于唤醒线程和阻塞线程,future的话,在这里就是一个用来获取线程数据的函数。

  对于C++11的并发编程部分(std::thread, std::future等)可以参考下面几篇文章进行学习:

  • 货比三家:C++ 中的 task based 并发:https://segmentfault.com/a/1190000002706259
  • 从 pthread 转换到 std::thread:https://segmentfault.com/a/1190000002655852

3.2 线程池类

  线程池类中:

  • 首先是一个构造函数,参数size_t是所传入的线程数。
  • 然后是一个enqueue的模板函数,emmm…,大佬就是大佬,这个函数真是大肠包小肠,具体看一看,其需要传入的参数是一个函数和函数参数,然后返回的是一个future,最后还通过一个type进行检测。
  • workers是工作线程
  • tasks是任务队列
class ThreadPool {
public:
    ThreadPool(size_t);
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) 
        -> std::future<typename std::result_of<F(Args...)>::type>;
    ~ThreadPool();
private:
   	// 需要跟踪线程,以便我们可以加入它们
    std::vector< std::thread > workers;
    // 任务队列
    std::queue< std::function<void()> > tasks;
    // 同步
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};

3.3 构造函数实现

  这个构造函数主要是用来启动一些工作线程,咋一看还不一定能看的懂,下面我们详细分析:

  • 首先在初始化列表中初始化stopfalse
  • 然后是一个线程数的for循环,循环里是一个很长的workers.emplace_back()语句,就是添加线程到工作线程组。再往里面则是一个lambda表达式。
  • lambda的函数体第一层是一个无线循环,用于线程内不断的从任务队列取任务执行,第二层有一个task就是一个函数对象,然后又一个大括号,刚开始我没看明白,但往下看就能懂,在这个大括号(大括号作用:控制lock临时变量的生命周期,执行完毕或return后自动释放锁)里面有:
    • std::unique_lock<std::mutex> lock(this->queue_mutex);加锁
    • this->condition.wait(lock, [this]{ return this->stop || !this->tasks.empty(); }); 等待条件成立(当线程池被关闭或有可消费任务时跳过wait继续;否则condition.wait将会unlock释放锁,其他线程可以继续拿锁,但此线程会阻塞此处并休眠,直到被notify_*唤醒,被唤醒时wait会再次lock并判断条件是否成立,如成立则跳过wait,否则unlock并休眠继续等待下次唤醒)
    • condition只是返回了false才会wait,也就是!stop && empty才会wait
// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t threads)
    :  stop(false)
{
    for(size_t i = 0;i<threads;++i)
        workers.emplace_back(
            [this]
            {
                for(;;)
                {
                    std::function<void()> task; //线程中的函数对象
                    // 通过lock互斥获取一个队列任务和任务的执行函数
                    {
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        this->condition.wait(lock,
                            [this]{ return this->stop || !this->tasks.empty(); });
                        if(this->stop && this->tasks.empty())
                            return;
                        // 任务队列的队首任务
                        task = std::move(this->tasks.front());
                        // 从队列移除
                        this->tasks.pop();
                    }
                    // 调用函数执行任务
                    task();
                }
            }
        );

}

3.4 入队—添加新的工作任务到线程池

  我们重新审视std::queue< std::function<void()> > tasks;,它只能接受void的函数类型,这里使用std::packaged_task<return_type()>完成函数类型的推导,因为这还不是最终放入tasks的对象,它要承接一个返回future的工作,而package_task就是来打包返回future的。

  其次将任务函数和其参数绑定,构建一个packaged_task(packaged_task是对任务的一个抽象,咱们能够给其传递一个函数来完成其构造。以后将任务投递给任何线程去完成,经过packaged_task.get_future()方法获取的future来获取任务完成后的产出值)

// add new work item to the pool
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) 
    -> std::future<typename std::result_of<F(Args...)>::type>
{
    using return_type = typename std::result_of<F(Args...)>::type;
    auto task = std::make_shared< std::packaged_task<return_type()> >(  //指向F函数的智能指针
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)  //传递函数进行构造
        );
    // 获取任务的future
    std::future<return_type> res = task->get_future();
    {
        // 独占拿锁
        std::unique_lock<std::mutex> lock(queue_mutex);

        // 不允许入队到已经停止的线程池 
        if(stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");
        // 将任务添加到任务队列
        tasks.emplace([task](){ (*task)(); });
    }
    // 发送通知,唤醒一个wait状态的工作线程重新抢锁并重新判断wait条件
    condition.notify_one();
    return res;
}

3.5 析构函数

// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
    {
        std::unique_lock<std::mutex> lock(queue_mutex); // 拿锁
        stop = true; // 停止标志
    }
    condition.notify_all(); // 条件变量进行通知
    // 等待所有工作线程结束
    for(std::thread &worker: workers)
        worker.join();
}

3.6 测试代码

#include <iostream>
#include <vector>
#include <chrono>

#include "ThreadPool.h"

int main()
{
    
    ThreadPool pool(4);
    std::vector< std::future<int> > results;

    for(int i = 0; i < 8; ++i) {
        results.emplace_back(
            pool.enqueue([i] {
                std::cout << "hello " << i << std::endl;
                std::this_thread::sleep_for(std::chrono::seconds(1));
                std::cout << "world " << i << std::endl;
                return i*i;
            })
        );
    }

    for(auto && result: results)
        std::cout << result.get() << ' ';
    std::cout << std::endl;
    
    return 0;
}

3.7 演示

  • 演示平台:腾讯云服务器 2 核 2 G,系统 Centos 7.6
  • 编译环境:GCC 9.1.0
    在这里插入图片描述

期待大家和我交流,留言或者私信,一起学习,一起进步!

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

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

相关文章

ModStartBlog v6.4.0 升级输入过滤、多文件组件,修复已知问题

V6.4.0版本更新 2022年12月20日ModStartBlog发布v6.4.0版本&#xff0c;增加了以下14个特性&#xff1a; [新功能] 富文本过滤规则调整优化[新功能] ArrayPackage数组输入数据包处理器[新功能] 请求输入组件新增多文件路径类型[新功能] 多文件上传组件[新功能] 所有组件新增t…

RCE代码及命令执行(详解)

RCE代码及命令执行1.RCE漏洞1.1.漏洞原理1.2.漏洞产生条件1.3.漏洞挖掘1.4.漏洞分类1.4.1.命令执行1.4.1.1.漏洞原理1.4.1.2.命令执行危险函数1.4.1.3.漏洞检测1.4.2.代码执行1.4.2.1.漏洞原理1.4.2.2.代码执行危险函数1.4.2.3.漏洞检测1.5.命令执行和代码执行区别2.命令执行2.…

QT学习记录(二)最基础的工程

文件 工程新建后会有这几个文件&#xff0c;自动生成的 main.cpp #include "mainwindow.h"#include <QApplication>int main(int argc, char *argv[]) {QApplication a(argc, argv);MainWindow w;w.show();return a.exec(); }QApplication a(argc, argv);这里…

Mentor-dft 学习笔记 day45-MTFI

Using MTFI Files此节介绍MTFI&#xff08;Mentor Tessend Fault Information&#xff09;功能&#xff0c;可用于ATPG工具和Tessent LogicBIST。MTFI是用于存储故障状态信息的通用且可扩展的文件格式。MTFI File Format MTFI是在“dft-edt”和“patterns-scan”上下文中读取和…

运用大O来给代码提速(冒泡排序)

本文内容借鉴一本我非常喜欢的书——《数据结构与算法图解》。学习之余&#xff0c;我决定把这本书精彩的部分摘录出来与大家分享。 本章内容 写在前面 1.冒泡排序 2.冒泡排序实战 3.冒泡排序的实现 4.冒泡排序的效率 5.二次问题 6.线性解决 7.总结 写在前面 大 O记…

Diffusion Model合集 part2

扩散模型原理介绍2五&#xff0c;逆扩散过程(Reverse Process)六&#xff0c;扩散过程中的后验的条件概率q(xt−1∣xt,x0)q(x_{t-1}|x_{t},x_{0})q(xt−1​∣xt​,x0​)七&#xff0c;目标数据分布的似然函数八&#xff0c;Diffusion Probabilistic Model的算法代码五&#xff…

“专利费用减缓”怎么申请?

在专利申请时&#xff0c;很多申请人对“专利费用减缓”的概念并不了解&#xff0c;或者不太清楚。甚至有很多申请人一听到专利申请可以请求费用减缓&#xff0c;就以为申请专利是不要钱的。当然&#xff0c;这样的理解就存在了很大的偏差了&#xff0c;所以&#xff0c;我们今…

Java Arrays类

JavaArrays类\huge{Java \space Arrays类}Java Arrays类 概述 Arrays类Arrays类Arrays类&#xff1a;本质就是一个工具类&#xff0c;用于操作数组元素的。 常用API ①. toString()&#xff08;重写过的toString方法&#xff09; toString()toString()toString()&#xff1a…

人才招聘网 招聘系统源码找工作源代码 各行业的招聘网站门户源码

运行环境&#xff1a;VS2015SqlServer2008R2.NET4.0 系统介绍&#xff1a; 适用于各地方或者各行业的招聘网站门户&#xff0c;相当强大的人才招聘网站&#xff0c;可登陆注册、支持第三方登陆&#xff0c;强大额后台管理&#xff0c;功能全面&#xff0c;界面美观 职位和简历…

如何用yolov5 训练自己的数据

文章目录 说明数据准备划分数据集转换数据label 格式训练前准备修改图片路径修改训练配置文件 voc.yaml修改 yolo配置文件开始训练没有GPU 或显存不够的说明 利用yolov5 训练自己的数据集通常需要利用自己标注的数据进行训练 接下来记录下如何训练自己的数据 数据准备 我这用的…

【数据结构】- 面试题

面试题1. 删除链表中的节点2. 反转一个链表&#xff08;非递归解法&#xff09;3. 判断一个链表是否有环&#xff08;快慢指针&#xff09;问题&#xff1a;[快慢指针为什么一定会相遇](https://blog.csdn.net/Leslie5205912/article/details/89386769)4. 获取单链表的节点个数…

【轻量级开源ROS 的机器人设备(5)】--(3)拟议的框架——µROS节点

前文链接&#xff1a;(2条消息) 【轻量级开源ROS 的机器人设备&#xff08;5&#xff09;】--&#xff08;2&#xff09;拟议的框架——ROS节点 五、静态栈分析 在处理运行多个资源的严格受限平台时线程&#xff0c;重要的是将堆栈使用保持在最低限度。这甚至 在利用具有同质堆…

技嘉电脑怎么开启vt模式?

电脑开启vt模式后&#xff0c;可以提高主板的运行速率&#xff0c;提高性能。那就有一些使用技嘉电脑的用户问技嘉主板怎么开启vt模式&#xff1f;下面小编就来教教大家技嘉电脑开启vt模式的方法。 Intel芯片组的技嘉主板 1、一般情况下&#xff0c;也就是在电脑开机的时候&…

计算机网络、操作系统刷题笔记15

计算机网络、操作系统刷题笔记15 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&#xff0c;你就得学数据库&#xff0c;sql&#xff0c;oracle&#xff0c;尤其sq…

差分详细讲解(C++)

每日一句:平凡的我在人多的地方曾极力小心翼翼&#xff0c; 但不知从何时起 &#xff0c;我不太在意别人的目光了。比起被人觉得是个怪人&#xff0c;我现在更害怕浪费时间。 差分一、一维差分二、二维差分一、一维差分 差分就是前缀和的逆运算,如果你不懂什么是前缀和,看这里…

移动技术在仓库运营管理中的作用

作者&#xff1a;Mike Kay&#xff0c;Mendix公司合作伙伴The Config Team渠道客户经理 市面上出现越来越多的仓库管理系统&#xff08;WMS&#xff09;以满足企业更好地管理供应链的需求。想要充分挖掘WMS的优点&#xff0c;一般可以通过移动解决方案来将关键的供应链运作进…

浪潮信息工程师:带你了解设备透传虚拟机的快速启动技术优化方案

编者按&#xff1a;将物理设备通过 vfio 透传给虚拟机是虚拟化常用的技术&#xff0c;但当为虚拟机分配比较大的内存时&#xff0c;虚拟机的启动时间会明显变慢&#xff0c;可能由十几秒延长至数分钟&#xff0c;严重影响用户使用体验。本文整理自龙蜥大讲堂 51 期&#xff0c;…

小林coding阅读笔记:计算机网络基础篇-TCP\IP模型

前言 参考/导流&#xff1a; 小林coding - 2.1 TCP/IP 网络模型有哪几层&#xff1f;学习意义 学习分层设计思想构建网络层次以及各层协议作用知识体系为网络编程奠定理论基础&#xff0c;对于RPC框架or分布式系统通信都是极为重要的一节&#xff0c;是提升整个系统效率的关键…

ubunt配置samba服务器,匿名访问

1. 环境 ubuntu14.04 2. 安装samba服务器 sudo apt-get install samba 3. 配置samba文件 vim /etc/samba/smb.conf 在最后添加如下内容 [muchx]comment Shared Folder with username and passwordpath /home/muchx/sharepublic yeswritable yesvalid users muchxcre…

二、基于kubeadm安装kubernetes1.25集群第一篇

1、概述 Kubeadm 是一个提供了 kubeadm init 和 kubeadm join 的工具&#xff0c; 作为创建 Kubernetes 集群的 “快捷途径” 的最佳实践。 kubeadm 通过执行必要的操作来启动和运行最小可用集群。 按照设计&#xff0c;它只关注启动引导&#xff0c;而非配置机器。同样的&…