【C++】异步(并发)实现 线程池 ---附源码+实现步骤(future、async、promise、package_task、任务池原理和框架)


每日激励:"加油ヾ(◍°∇°◍)ノ゙陌生人"

早关注不迷路,话不多说安全带系好,发车啦(建议电脑观看)。**

思维导图:

C++11异步操作所需函数

1. std::future:

std::future是C++11标准库中的⼀个模板类,它表⽰⼀个异步操作的结果。当我们在多线程编程中使⽤异步任务时,std::future可以帮助我们在需要的时候获取任务的执⾏结果。std::future的⼀个重要特性是能够阻塞当前线程,直到异步操作完成,从⽽确保我们在获取结果时不会遇到未完成的操作。

应⽤场景

  • 异步任务: 当我们需要在后台执⾏⼀些耗时操作 时,如⽹络请求或计算密集型任务等,std::future可以⽤来表⽰这些异步任务的结果。通过将任务与主线程分离,我们可以实现任务的并⾏处理,从⽽提⾼程序的执⾏效率
  • 并发控制: 在多线程编程中,我们可能需要等待某些任务完成后才能继续执⾏(需要阻塞等待)其他操作。通过使⽤std::future,我们可以实现线程之间的同步,确保任务完成后再获取结果并继续执⾏后续操作
  • 结果获取:std::future提供了⼀种安全的⽅式来获取异步任务的结果 。我们可以 使⽤std::future::get()函数来获取 任务的结果,此函数会阻塞当前线程,直到异步操作完成。这样,在调⽤get()函数时,我们可以确保已经获取到了所需的结果

2. std::async关联异步任务:

std::async关联异步任务,任务结果关联的std::future对象。默认情况下,std::async是否启动⼀个新线程,或者在等待future时,任务是否同步运⾏都取决于你给的参数。

这个参数为std::launch类型:

  1. std::launch::deferred表明该函数会被延迟调⽤,直到在future上调⽤get()或者wait()才会开始执⾏任务
  2. std::launch::async 表明函数会在⾃⼰创建的线程上运⾏
  3. std::launch::deferred | std::launch::async 内部通过系统等条件⾃动选择策略

2.1 future + async 实操同步和异步操作:

async.cc文件、头文件:thread、future

  1. Add加法函数
    1. 打印:加法(测试查看)

主函数:

  1. 将加法操作进行异步操作!
    1. async(policy,func,...) 策略+回调函数+args...
    2. 参数:std::lacunch::deferred策略(同步)、Add回调函数、回调函数的参数args...
    3. 使用std::this_thread::slepp_for(std::chrono::seconds(1)休眠1s)进行测试使用,查看打印的信息。
    4. 返回值是future<(Add函数的返回值类型)> 接收成fu对象变量
  2. fu调用get函数,获取结果
    1. deferred:在执行get获取异步结果的时候才会执行异步任务
  3. 打印结果

策略换成std::launch::async

此时就能发现async会直接进行异步操作
(内部创建工作线程,异步的完成任务,而非在get处才进行异步)

查看结果 进行了主线程和回调函数的异步操作:

代码:

csharp 复制代码
#include <iostream>
#include <thread>
#include <future>
#include <unistd.h>

int Add(int n1,int n2)
{
    std::cout << "------进行异步操作------" << std::endl;
    return n1 + n2;
}


int main(){
    //实现将加法函数使用future进行异步操作
    //defferred策略
    //在future对象get获取时才进行异步操作
    std::future<int> fu = std::async(std::launch::async,Add,11,12);
    // std::future<int> fu = std::async(std::launch::deferred,Add,11,12)
    std::this_thread::sleep_for(std::chrono::seconds(1));//休眠1s
    std::cout << "------1------" << std::endl;
    //当使用deferred策略时,异步操作会在get处执行
    int sum = fu.get();
    std::cout << sum << std::endl;
    return 0;
}

3. std::promise::get_future和std::future配合:

std::promise提供了⼀种设置值的⽅式,它可以在设置之后通过相关联的std::future对象进⾏读取。换种说法就是之前说过std::future可以读取⼀个异步函数(async)的返回值, 但是要等待就绪, ⽽std::promise就提供⼀种⽅式⼿动让std::future就绪。

可能有点抽象,通过代码慢慢品。

3.1 promise + future的实操:

promise.cc、头文件:thread、future
加法函数:

  1. int Add(n1,n2,promise &prom)
    1. 调用sleep_for休眠3s(通过睡眠3s,来展现pro设置数据与fu获取数据的同步关系)
    2. prom调用set_value设置结果
    3. return n1 + n2;

主函数:

  1. promise对象
  2. prom调用get_future获取std::future对象给到fu对象
  3. 使用std::thread类构造线程调用Add函数、n1、n2、std::ref获取prom引用
cpp 复制代码
  std::thread(Add,11,12,&pro);//使用线程调用Add函数,进行异步操作
  1. 使用fu对象调用get获取结果
    1. 获取的结果其实就是prom设置的结果,也就是在线程中promise对象设置数据,
    2. 这样就实现了其他线程通过future获取数据的获取异步任务结果的功能。
    3. 该promise设置结果和future获取结果会存在一个同步关系,也就是必须要等待到promise设置完结果后,才能get获取到结果,保证一定有结果!!
  2. 打印查看sum
  3. 注意线程终止join

看完实现步骤,在总结下:

promise的使用不像async一样自身去实现同步和异步策略的然后返回结果。promise他是是将自身传递给异步线程,在异步线程中获取参数,而外部的future对象是提前和promise关联的,关联后当future对象想获取数据时通过get获取,但该get是保证和关联的promise是同步的,所以即使没有数据future的get也会等待promise设置完成数据,最终通过future对象成功的获得异步的数据。

具体细节见代码注释:

cpp 复制代码
#include <iostream>
#include <thread>
#include <future>

int Add(int n1,int n2,std::promise<int>& p)
{
    std::cout << "-----等待promise-----" << std::endl;
    //睡眠3s
    std::this_thread::sleep_for(std::chrono::seconds(3));//chrono英文:计时
    //证明是不是 fu的get在等待 pro的set_value
    p.set_value(n1+n2);
    return n1 + n2;
}

int main(){
    //demo测试:promise 与 future的结合使用

//模板内填写的是:调用函数的返回值类型,int Add(...)
    std::promise<int> pro;
    //将future对象 fu 与 promise对象 pro 进行关联。
    std::future<int> fu = pro.get_future();

    std::thread th(Add,11,12,std::ref(pro));//使用线程调用Add函数,进行异步操作
    //ref函数获取pro的引用
    std::cout << "-----1-----" << std::endl;
    int sum = fu.get();//此时并没有使用async函数,而是通过pro与fu对关联,fu从pro设置的值中获取异步数据
    //并且fu的get与pro的set_value是同步的!!
    std::cout << "-----2-----" << std::endl;
    std::cout << sum << std::endl;

    th.join();//关闭线程
    return 0;
}

4. packaged_task(任务包)

std::packaged_task就是将任务和 std::future 绑定在⼀起的模板是⼀种对任务的封装。通过std::packaged_task对象获取任务相关联的std::future对象(packaged_task调⽤get_future()⽅法获得)

std::packaged_task的模板参数是函数签名(就是返回值+参数(省略函数名和参数名),如:void(int)、int(double,double))。可以把std::future和std::async看成是分开的, ⽽std::packaged_task则是⼀个整体,将一个函数给封装起来,也能返回一个future对象将函数的结果保存起来。
总结:

packaged_task 可以通过get_future获取一个future对象,并获取获取封装的这个函数的异步结果

4.1 future+paskaged_task实操:

package_task.cc、头文件:thread、future

加法函数

  1. int Add(n1,n2)
    1. sleep_for 3s
    2. return结果(n1+n2)

主函数:

  1. 使用packaged_task<回调函数签名> task对象,构造中填回调函数名(Add)进行二次封装到task中
  2. 使用task的get_future函数获取fu对象

注意不能把task当成正常函数,也就是不能当作async异步执行函数、也不能当作线程入口

task只可以当作可调用来调用执行任务:也就是直接调用函数的方式:如task(...)。
注意,指针指向对象的生命周期问题(不能是局部变量!),所以需要使用指针类型,通过new对象将他放在堆上,也就是用智能指针管理它的生命周期。

这样就把task定义为一个指针,传递到线程中后,进行解引用执行(注意* 与 括号间的优先级问题!要写成:(*task)(n1,n2) )

这样重新写:将上面的局部成员变量的task改成智能指针:

  1. 将packaged_task<函数签名>对象用智能指针ptask对象储存(直接使用auto),并且使用make_shared构造回调函数。
  2. 使用该对象指针,调用get_future生成关联的future函数
  3. 使用线程 thr 对象调用函数(使用lambda表达式[&] ( ) {...})
    1. 内部调用回调函数(类似C++仿函数调用不过参数是指针,就类似C语言中的函数指针的使用)
  4. 使用关联的fu对象的get函数获取结果、打印sum结果
  5. 关闭线程

package_task使用的具体代码:

cpp 复制代码
#include <iostream>
#include <thread>
#include <future>

int Add(int n1,int n2)
{
    std::cout << "-----等待promise-----" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(3));//chrono英文:计时

    return n1 + n2;
}

int main(){
    // std::packaged_task<int(int,int)> task(Add);//将Add函数进行 二次封装
    // task(11,12);
    // std::future<int> fu = task.get_future();

    //使用智能指针替代直接创建变量:

    // std::shared_ptr<std::packaged_task<int(int,int)>> ptask = std::make_shared<std::packaged_task<int(int,int)>>(Add);
    //智能指针的类型:std::packaged_task<int(int,int)>、其中make_shared构造的时候也需要填写该类型!
    auto ptask = std::make_shared<std::packaged_task<int(int,int)>>(Add);
    std::future<int> fu = ptask->get_future();

    std::thread thr([&](){
        (*ptask)(11,12);

    });
    int sum = fu.get();
    std::cout << sum << std::endl;

    //注意关闭线程
    thr.join();
    return 0;
}

线程池的实现

基于线程池执⾏任务的时候,⼊⼝函数内部执⾏逻辑是固定的,因此选择std::packaged_task加上std::future的组合来实现线程池。

线程池的工作思想:
用户传入要执行的函数,以及处理的数据(参数),有线程池中的工作线程来执行函数完成任务

实现框架:

  1. 需要的管理成员
    1. 任务池:用vector维护的函数池子
    2. 实现同步互斥:互斥锁、条件变量
    3. 一定数量的工作线程:用于不断从任务池取出任务中执行任务
    4. 结束运行标志
  2. 需要的管理操作
    1. 入队任务:入队一个函数和参数
    2. 停止运行:停止线程池

具体实现线程池

1. 先搭线程池的框架:

threadpool.hpp文件、ThreadPool类:

成员变量:

  1. 结束标志:_stop(原子的)
  2. 锁:_mutex
  3. 条件变量:_cv(conditino_variable)实现同步操作
  4. 工作线程:_threads(vector储存)线程一定要放在条件变量和锁下面
  5. 任务池< Functor >:_taskpool

成员函数:

  1. 任务池中的任务:using声明 Functor任务:function<void(void)>

  2. 构造(thr_count = 1)

  3. 析构

  4. 加入线程池的push函数

    1. 需要在此了解的内容.
      模板函数<F,Args不定参>push(F && func,Args&& ...args)
    2. 入队给线程池,push的是一些任务,怎么知道函数的参数?
    3. 使用模板参数 template<函数类型 F、不定参数 ...Args> 获取函数的类型和参数
    4. auto push(F && func, Args && ...args) - > std::future< decltype ( func ( argc... ) ) >(其中->箭头就是用于返回值类型的说明)
    5. 其中push传入的首先第一个参数是一个要执行的函数、第二个参数是一个不定参(不定个数的参数,也就是该函数所需要的所有参数)
    6. push函数内部,会将这个传入的函数封装成一个异步任务(使用packaged_task任务包),再生成关联的future,将他返回回去这样就能在外部获取最终结果
    7. 因为不知道push函数的返回值类型,所以返回值写成auto,再使用decltype通过内部的函数推导,推导出返回值的类型给到函数的返回值处。
    8. 使用lambda生成一个可调用对象(内部执行异步任务),抛入到任务池中(后面由工作线程取出执行)
  5. viod stop

私有函数:

  1. 线程入口函数:void entry()(内部不断的从任务池中取出任务进行执行)

2. 线程池的实现流程:

  1. 构造(thr_count = 1):

    1. stop(false)
    2. 创建线程池中的线程:emplace_back原地构造内部创建指定数量的线程(构成线程所要执行的函数,也就是调用线程入口函数)
  2. 析构

    1. 调用stop
  3. stop:

    1. 判断若stop为true了就直接返回了,不要进行重复退出
    2. 结束符号:_stop修改为true
    3. 唤醒所有工作线程:_cv调用notify_all() (V操作)
    4. 等待所有线程的退出:
    5. 遍历_threads,进行thread的join
  4. auto push(F && func, Args && ...args) - > std::future< decltype ( func ( args... ) ) > (模板:template<typename F,typename ...Args>)

    1. 将传入的函数封装成一个packaged_task任务包
      1. 使用bind包装器绑定(forward(func),forward(args)...),生成一个可调用对象tmp_func(并且这个函数是不需传递参数的,他已经提前绑定进去了),其中使用的forward是进行完美转发的(forward会根据传进来的参数,区分右值和左值)),最后的(args)...是一个参数包,能发多个参数
      2. using声明函数的return_type返回值的类型,同样通过decltype推导出传递进来的func函数的返回值类型(给到packaged_task封装任务包使用)
      3. 创建packaged_task任务包对象task,构造把tmp_func函数给到任务,并且封装成智能指针(因为防止局部变量在线程中执行时被销毁)使用make_shared创建,此处不需要写参数(也就是packaged_task<函数类型写成:return_type( )>),因为已经bind绑定进去了。
    2. 获取task任务包关联的future对象(使用get_future)
    3. 构造一个lambda匿名函数(捕获任务对象),函数内执行任务对象。
      1. 加锁:{ //使用括号进行限定作用域(为了让锁在局部有效,处作用域后自动解锁)
      2. 使用 unique_lock< mutex >类lock对象构造为_mutex,直接进行加锁(加锁方法: std::unique_lock<std::mutex> lock(_mutex);//这样就完成了加锁!! 对_mutex变量,直接进行了加锁操作
    4. 将构造出来的匿名函数对象,抛入到任务池中:任务池push_back(vector中的函数)储存匿名对象lambda[ ] ( ){ }表达式(内部执行任务操作,也就是 (*task) (); )
    5. _cv唤醒线程(执行entry函数),nodify_one
      3. }(此处对应第二点的加锁括号)
    6. 返回fu对象(这样外部也能通过future的获得异步数据)
  5. void entry()函数:

    1. 加锁
      1. 同样使用unique_lock
    2. 等待任务池不为空,或_stop被置位返回
      1. cv调用wait进行等待,参数:
      2. 传递锁lock
      3. 自动唤醒条件函数:
      4. [ this ] ( ) { return _stop || !_taskpool.empty(); } //当stop为真或者线程池不为空的时候
    3. 取出任务执行
      1. 但直接取每次只能取出一个任务(只取出一个任务,若任务池中,还有就会频繁的去取,而取任务就会重复上诉过程,就会频繁加锁,加锁就会影响整体效率)
      2. 所以需要改变策略,取任务的方法处限定作用域
      3. 首先在作用域外创建一个临时的任务池
      4. 其次在作用域内将零时的任务池和真正的任务池进行swap(这样就能一次将所有的任务都取出来)
      5. 最后出作用域,遍历临时的任务池,执行所有任务task()(也就是push函数中任务池push_back储存的匿名函数)
    4. 任务的循环处理最外层一定要死循环,直到stop被置位(while(!stop))

加法函数(略)

主函数:

  1. ThreadPool pool;//线程池
  2. 循环10个任务
    1. fu对象 获取 pool调用的push(加法函数)方法返回来的关联future
    2. fu调用get查看结果,并打印查看
  3. poolstop
cpp 复制代码
#include <iostream>
#include <mutex>
#include <atomic>
#include <condition_variable>
#include <vector>
#include <functional>
#include <thread>
#include <future>


class ThraedPool{


public:
    ThraedPool(int thr_count = 1):_stop(false){
        //创建对应的线程个数
        for(int i = 0 ; i < thr_count ;i++){
            _threads.emplace_back(std::thread(&ThraedPool::entry,this));//注意函数的 取地址 
            //线程的初始化构造函数:会自动执行entry,参数就是后面的args
        }
    }

    ~ThraedPool(){
        stop();
    }
    
    void stop(){
        if(_stop == true) return;//防止重复
        _stop = true;//设置结束标志

        _cv.notify_all();//唤醒所有工作线程,P操作
        //等待所有线程退出
        for(auto& thread : _threads){
            thread.join();
        }
    }

    template<typename F,typename ...Args>//F:函数、Args:参数
    auto push(const F &&func,Args && ...args) -> std::future<decltype(func(args...))>
    {
        //推导出函数的返回值类型,同样使用decltype
        using return_type = decltype(func(args...));

        //使用bind函数适配器,提前将参数进行绑定,这样就能剩去参数的传递
        auto tmp_func = std::bind(std::forward<F>(func),std::forward<Args>(args)...);//注意此处要把...参数包放到外面
//其中使用到了 forward 进行完美转发,防止右值引用被当成左值进行传递

        //使用智能指针将,packaged_task任务包进行封装,并且把func执行的函数给到任务包,此处传递进行封装后的函数tmp_func
        auto task = std::make_shared<std::packaged_task<return_type()>>(tmp_func);

        std::future<return_type> fu = task->get_future();//获取关联的future对象
        //构造匿名lambda表达式进行捕获对象,并执行
        //对于共用成员,需要加锁处理:
        {
            //加锁方法使用 unique_lock,具体如下:
            std::unique_lock<std::mutex> lock(_mutex);//加锁完成!
            //进行异步操作
            //3. 将构造出来的匿名函数对象,抛入到任务池中
            _taskpool.push_back([task](){
                (*task)();
            });
            _cv.notify_one();//唤醒线程
        }
        return fu; //返回future对象       
    }


    using Factor = std::function<void(void)>;
private:
//线程的入口内部不断的从任务池中取出任务进行执行:
    void entry(){
        while(!_stop){
            std::vector<Factor> tmp_taskpool;
            //1. cv 进行等待,等待任务到来,或者 stop结束标志被置位 或者 任务池为空
            {//加锁的作用域:
                std::unique_lock<std::mutex> lock(_mutex);
                _cv.wait(lock,[this](){
                    //唤醒条件
                    return _stop || !_taskpool.empty();
                });
                //唤醒后执行,但这样只能执行一个,所以在作用域外创建局部任务池
                tmp_taskpool.swap(_taskpool);//与全局的任务池交换,获取所有任务
            }
            //遍历执行任务
            for(auto& task : tmp_taskpool){
                task();//执行任务
            }
        }
     }
private:
    //1. 互斥锁
    std::mutex _mutex;
    //2. 条件变量
    std::condition_variable _cv;
    //3. 执行的任务池
    std::vector<Factor> _taskpool;
    //4. 线程池
    std::vector<std::thread> _threads;
    //5. 结束标志
    std::atomic<bool> _stop;
};

int Add(int n1, int n2){
    return n1 + n2;
}

int main(){
    //创建线程池
    ThraedPool pool;

    for(int i = 0 ; i < 10 ;i++){
        auto fu = pool.push(Add,11,i);
        std::cout << fu.get() << std::endl;
    }
    
    pool.stop();

    return 0;
}

总结结合实例,理清下线程池工作的原理:

  1. 创建一个线程池TreadPool,他内部会先构造出线程池(多个线程,他们都会不断的执行entry)和任务池
  2. 执行push将任务交给线程池,push就会将这些任务进行打包(package_take)并丢进任务池中,后唤醒线程(也就是唤醒entry中的wait处,进行从任务池中获取任务执行),并且返回一个future对象给到主函数
  3. 这样外部的主函数和内部的线程就能实现异步操作,主函数就能直接通过future的get获取到数据(主函数他获取的数据和线程池中执行任务后的数据 是同步的)

最终结果


本章完。预知后事如何,暂听下回分解。

如果有任何问题欢迎讨论哈!

如果觉得这篇文章对你有所帮助的话点点赞吧!

持续更新大量c++细致内容,早关注不迷路。

相关推荐
007php00721 分钟前
linux服务器上CentOS的yum和Ubuntu包管理工具apt区别与使用实战
linux·运维·服务器·ubuntu·centos·php·ai编程
网络风云30 分钟前
【魅力golang】之-反射
开发语言·后端·golang
Q_192849990634 分钟前
基于Spring Boot的电影售票系统
java·spring boot·后端
qq_429856571 小时前
linux 查看服务是否开机自启动
linux·运维·服务器
我要学编程(ಥ_ಥ)1 小时前
初始JavaEE篇 —— 网络原理---传输层协议:深入理解UDP/TCP
java·网络·tcp/ip·udp·java-ee
就爱学编程1 小时前
重生之我在异世界学编程之C语言:数据在内存中的存储篇(下)
java·服务器·c语言
小哈龙1 小时前
c++ 类似与c# 线程 AutoResetEvent 和 ManualResetEvent的实现
c++·c#·多线程
百事可乐☆2 小时前
全局webSocket 单个页面进行监听并移除单页面监听
网络·websocket·网络协议
深圳启明云端科技2 小时前
WiFi、蓝牙共存,物联网无线通信技术,设备无线连接数据传输应用
网络·物联网·智能家居
运维&陈同学2 小时前
【Kibana01】企业级日志分析系统ELK之Kibana的安装与介绍
运维·后端·elk·elasticsearch·云原生·自动化·kibana·日志收集