首页 > 编程语言 >C++并发编程:线程池学习

C++并发编程:线程池学习

时间:2024-03-14 21:31:16浏览次数:36  
标签:std task 函数 编程 C++ 任务 线程 模板

文章目录


最近学习C++并发编程,发现一个star量很高的 线程池项目,写的短小精悍,想要分享给大家
该项目真的简洁明了,线程池所有的实现均在 ThreadPool.h里面,短短100行左右的代码,其中最难理解的并不是并发部分,反而线程池实现中大量的CPP11语法糖才是难点,接下来就一起来看看吧

一、线程池的概念

线程池(Thread Pool)是一种并发编程的设计模式,它维护了一个线程的集合,用于执行提交给它的任务,线程池中的线程可以被重复使用,以避免为每个任务创建和销毁线程的开销,有以下好处:

  1. 提高性能—(线程的创建和销毁涉及到系统资源的分配和释放,产生较大开销,通过重复利用已创建的线程,避免频繁创建和销毁线程,提高系统性能和速度)
  2. 控制并发度—(限制并发执行的线程数量,防止过载,通过控制线程池的大小,可以根据系统资源和负载情况来合理调整并发度,避免资源竞争)
  3. 提供任务队列—(任务队列存储待执行的任务,可以定制策略平衡任务生产和消费速度,避免任务过多导致系统负载过高,提供了任务调度和管理的灵活性,可以定制自由的调度策略

二、线程池的设计

实现一个线程池基本包含以下步骤:

  1. 创建线程池(确定线程池的大小,即同时可执行的线程数量)
  2. 创建任务队列(用于存储待执行的任务)
  3. 初始化线程池(创建指定数量的线程,并将它们置于等待状态)
  4. 提交任务(将任务提交到任务队列中)
  5. 线程执行任务(线程从任务队列中获取任务并执行)
  6. 线程池管理(包括动态调整线程池大小、监控线程状态、处理异常等)
  7. 销毁线程池(释放线程池中的资源,包括线程和任务队列)

在实现线程池时,需要考虑线程的同步与互斥,以及任务队列的管理和调度
all in all,线程池是一种管理和复用线程资源的机制,通过提供线程池,可以更好地控制并发度和提高系统性能,针对这种思想与这个开源项目,绘制如下流程图,通过 std::condition_variable 控制任务队列与线程容器之间的同步关系,通过 std::mutex 控制任务队列的访问,保证资源的独占访问
请添加图片描述

三、线程池的实现

1、ThreadPool声明
class ThreadPool {
public:
    ThreadPool(size_t);
    ~ThreadPool();

    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>;

private:
    // need to keep track of threads, so we can join them
    std::vector< std::thread > workers;
    // the task queue
    std::queue< std::function<void()> > tasks;
    // synchronization
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};

重点解释下下面这行代码

template<class F, class... Args>
auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>;

enqueue() 函数是将一个 task 封装到 tasks 任务列表,下面这行代码是一个函数模板,F 是一个可调用对象的类型(函数、函数指针、函数对象等),Args 是可调用对象 f 的参数类型,class… Args 是 C++ 中的可变参数模板(variadic template)语法,模板中的“&&”代表万能引用,既能接收左值又能接收右值
该函数模板返回值为:std::future<std::result_of>,这是一个 std::future 对象,同时这是一个模板类 std::future 的实例化,它将模板参数设置为 F(Args...)这个可调用对象的返回类型
std::result_of是一个模板元函数,用于推断函数类型 F 在给定参数类型 Args… 的情况下的返回类型,::type 用于获取模板元编程中的类型,typename 关键字来指示 std::result_of<F(Args…)> 是一个类型而非成员变量或成员函数
还有一个疑问???
为什么 (-> std::future<typename std::result_of<F(Args…)>::type>) 不是已经指明了返回类型吗,为什么函数前面还要加一个 auto 呢???
auto 关键字用于推断函数 enqueue 的返回类型,它允许编译器根据函数体中的表达式推导出函数的返回类型,而不需要显式指定返回类型
函数 enqueue 的返回类型将根据 F 和 Args… 推断出来,并且是一个 std::future 类型,其模板参数是通过 std::result_of<F(Args…)>::type 推导而来的
需要注意的是,std::result_of 在 C++17 中已被弃用,推荐使用 std::invoke_result 替代
综上所述,这个函数模板返回一个 std::future 对象,该对象封装了函数 F 在给定参数类型 Args… 下的返回类型,用于异步获取函数的返回值

2、线程创建

关于为啥使用 emplace_back 而非 push_back,可以去查阅下面的相关知识点
使用了Lambda表达式、std::unique_lock智能锁、std::condition_variable条件变量
直接向 workers 里面加入一个 Lambda 表达式,是一个无限循环,进入之后阻塞【 condition.wait 】
直到拿到 queue_mutex 锁 并且 程序未停止或任务队列不空,然后继续判断【 if (this->stop && this->tasks.empty()) 】,这个判断程序是否停止,如果还有任务的话,会将任务执行完
在 ThreadPool 中,stop标志量置为真并唤醒所有睡眠线程,将队列中的第一个元素出队并赋给 task(std::function<void()> 表示一个可调用对象),之后代码块结束,释放 queue_mutex 锁
最后执行task

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;

                    {
                        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、添加任务
  1. 推导可执行对象【f(…args)】的返回类型
  2. 将可执行对象转为【std::packaged_task】指针
  3. 获取任务的异步操作的结果【std::future】
  4. 将任务插入队列,无需~~【condition.wait()】~~
  5. 唤醒一个工作线程【condition.notify_one()】

Q1:为啥 std::packaged_task<return_type()> 里面 return_type 使用要加上()?
A1:std::packaged_task是一个模板类,用于封装可调用对象或函数,并将其作为异步任务进行管理,注意,return_type()使用了圆括号(),这是为了表示函数的签名,即使函数没有参数

Q2:std::bind(std::forward(f), std::forward(args)…) 语法剖析
A2:1、std::bind是一个函数模板,用于创建一个可调用对象(函数对象或函数指针)的绑定副本,可以将函数对象与一些参数进行绑定,生成一个新的可调用对象;2、std::forward是一个用于完美转发(perfect forwarding)的转发函数模板,用于在函数模板中保持传递的参数类型的右值或左值特性;3、回到代码,std::bind(std::forward(f), std::forward(args)…)是将可调用对象f和参数args…进行绑定,并生成一个新的可调用对象

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()> >(
        std::bind(std::forward<F>(f), std::forward<Args>(args)...)
    );

    std::future<return_type> res = task->get_future();
    {
        std::unique_lock<std::mutex> lock(queue_mutex);

        // don't allow enqueueing after stopping the pool
        if (stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");

        tasks.emplace([task]() { (*task)(); });
    }
    condition.notify_one();
    return res;
}
4、ThreadPool析构
inline ThreadPool::~ThreadPool()
{
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();
    for (std::thread& worker : workers)
        worker.join();
}

四、相关知识点

1、emplace_back 和 push_back

push_back 函数是在容器的末尾添加一个新元素,通过将该元素的副本(或移动语义)插入容器
emplace_back 函数以构造函数的参数直接在容器的末尾构造一个新元素,而不是通过副本或移动语义
后者避免了不必要的移动和拷贝操作

2、typename std::result_of<F(Args…)>::type

一种模板元编程中的语法,用于获取函数类型 F 的返回值类型
std::result_of<F(Args…)> 是一个类型,它表示调用函数类型 F 并传递参数类型 Args… 后的返回类型
::type 是一个成员类型,用于访问 std::result_of<F(Args…)> 推导出的类型

3、std::packaged_task<return_type()>

std::packaged_task<return_type()> 是 C++ 标准库中的一个模板类,用于包装可调用对象(如函数、函数对象或 Lambda 表达式)并将其封装为异步任务

4、函数模板 和 模板函数 这两个有什么区别
  • 函数模板是一种通用的函数模板定义,可以根据不同的类型参数生成多个具体的函数实例
  • 模板函数(或特化函数)则是对函数模板的特殊化,用于提供对特定类型或特定类型集合的定制行为

总结起来,函数模板是一种通用的函数定义,可以根据不同的类型参数生成多个具体的函数实例;而模板函数(特化函数)则是对函数模板的特殊化,用于提供对特定类型或特定类型集合的定制行为
函数模板提供了泛型编程的能力,而模板函数为特定类型提供了定制化的实现

5、线程通知机制

利用锁和条件变量,我们可以实现线程通知机制
线程通知机制指的是,刚开始时线程池中是没有任务的,所有的线程都等待任务的到来,当一个任务进入到线程池中,就会通知一个线程去处理到来的任务
线程池与任务队列之间的匹配操作,是典型的生产者-消费者模型

6、锁 和 条件变量

mutex—锁:保证任务的添加和移除(获取)的互斥性,即同一时间职能有一个线程添加或移除任务
condition_variable —条件变量:保证多个线程获取task的同步性,即当任务队列为空时,线程应该等待(阻塞)

7、std::function 介绍

类模版 std::function 是一种通用、多态的函数封装,可以对任何可以调用的目标实体进行存储、复制、和调用操作,这些目标实体包括普通函数、Lambda表达式、函数指针、以及其它函数对象等
std::function 对象是对C++中现有的可调用实体的一种类型安全的包裹(我们知道像函数指针这类可调用实体,是类型不安全的)

8、Lambda的规则

Lambda 表达式可以捕获外部变量,即在函数体中使用定义在Lambda表达式之外的变量,通过捕获外部变量,Lambda表达式可以在函数体中使用这些变量。

[capture list] (parameter list) -> return type { function body }

- capture list 指定了Lambda表达式可以访问的外部变量,可以省略。
- parameter list 是Lambda表达式的参数列表,可以为空。
- return type 是Lambda表达式的返回类型,可以省略,编译器会自动推导。
- function body 是Lambda表达式的函数体。

下面写了一个简单示例

#include <iostream>

int main() {
    int a = 2;
    int b = 3;

    auto sum = [a, b] () {
        return a + b;
    };

    std::cout << "Sum: " << sum() << std::endl;

    return 0;
}

标签:std,task,函数,编程,C++,任务,线程,模板
From: https://blog.csdn.net/qq_45797625/article/details/136708985

相关文章

  • C 语言多文件编程
    C语言中的多文件编程通常涉及将代码分散在几个不同的源文件(.c文件)和头文件(.h文件)中。这么做可以帮助你组织大型项目,提高代码的重用性,便于团队合作,分离接口和实现,以及加快编译时间。下面是一个多文件编程的基本步骤:创建头文件:头文件通常包含结构体定义、全局变量声明、常量定......
  • C/C++ vscode 配置
    一、由于vscode本身不带有编译器,需要下载MinGW编译器 打开网站:MinGW-w64-for32and64bitWindows-Browse/mingw-w64/mingw-w64-releaseatSourceForge.net下载x86_64-win32-seh版本下载后,解压缩,把解压缩后的文件剪切奥C:\ProgramFiles把路径C:\ProgramFiles......
  • C++函数模板的重载
    C++模板当需要对不同的类型使用同一种算法(同一个函数体)时,为了避免定义多个功能重复的函数,可以使用模板。然而,并非所有的类型都使用同一种算法,有些特定的类型需要单独处理,为了满足这种需求,C++允许对函数模板进行重载,程序员可以像重载常规函数那样重载模板定义。我们定义了Swap(......
  • IntelliJ IDEA 中用scala编程
    目录二.测试Scala插件    九九乘法表三.spark环境的配置    进入IntelliJIDEA界面,点击右下角绿色的插件点击 Configure...搜索Plugins找到scala并下载好点击RestartIDE重启一.测试Scala插件1.点击newProject选项2.点击进去,选择左侧scal......
  • C++ error C2143: 语法错误: 缺少“;”(在“*”的前面)
    errorC2143编译错误但是,我在官网的例子上没有找到我所遇见的问题!在此记录一下,问题代码如下:1classtestA1;2classworkclass3{4public:5explicitworkclass();6virtual~workclass();7private:8intM_INT;9......
  • c++面试必问20题
    引用为什么不能修改引用关系?什么是重载this指针如何在类中出现的?类中的函数存放在代码区,所有对象访问的成员函数都是同一份代码,当不同对象调用同一个成员函数时,通过this区分在成员函数内修改的是哪个对象的成员变量this指针是否可以修改?不可以,如果修改了this就无法在函数......
  • Go语言中的面向对象编程(OOP)
    在Go语言中,虽然没有像面向对象语言那样的类,但通过结构体类型和方法,仍然支持部分面向对象编程(OOP)的概念。封装(Encapsulation)封装是一种将一个对象的实现细节隐藏起来,使其对其他对象不可见的做法,这样可以实现解耦。例如,考虑以下结构体:typeStudentstruct{namestring......
  • C++动态数组
    #include<iostream>usingnamespacestd;intmain(){ intt,i=0,j=0; cin>>t; char*pc=nullptr;//初始化 int*pi=nullptr;//初始化 float*pf=nullptr;//初始化 intsum=0; intFLAG=0; while(FLAG<t) { charch; cin>>......
  • 网络编程1 基础概念+Socket编程
    网络编程1基础概念+Socket编程协议概念:协议事先约定好,大家共同遵守的一组规则,如交通信号灯.从应用程序的角度看,协议可理解为数据传输和数据解释的规则;可以简单的理解为各个主机之间进行通信所使用的共同语言.分层模型OSI是OpenSystemInterconnection的缩写,意......
  • 04_C++字符串_迭代器使用
    概念:迭代器是一种检查容器内元素并遍历元素的数据类型,通常用于对C++中各种容器内元素的访问,但不同的容器有不同的迭代器,初学者可以将迭代器理解为指针。1.使用迭代器使用begin和end,begin成员负责返回第一个元素(或者第一个字符)的迭代器。end成员返回指向容器“尾元素的下一个位置......