跳转至内容
  • 版块
  • 最新
  • 标签
  • 热门
  • Online Tools
  • 用户
  • 群组
折叠
品牌标识

D2Learn Forums

  1. 主页
  2. SubForums
  3. 现代C++ | mcpp论坛
  4. 【C++11】异步操作

【C++11】异步操作

已定时 已固定 已锁定 已移动 现代C++ | mcpp论坛
c++11
2 帖子 2 发布者 13 浏览
  • 从旧到新
  • 从新到旧
  • 最多赞同
登录后回复
此主题已被删除。只有拥有主题管理权限的用户可以查看。
  • LyuihL 离线
    LyuihL 离线
    Lyuih
    编写于 最后由 编辑
    #1

    我的知乎主页:https://www.zhihu.com/people/lin-xi-44-16-84

    1. 同步与异步

    核心逻辑

    同步:

    • 做完一件事情,才能做下一件事情。
      异步:
    • 不用等这一件事情做完,先去做别的事情,等它好了Q再通知你。

    举个例子就是:
    同步就像你在食堂排队打饭,你必须等待前面的人都打完了并离开才轮到你,在此期间不只能排队,什么也干不了。

    异步就像你点外卖,你不需要站在店里面等待,可以干其他事情,甚至可以同时点几份外卖,当外卖做好时,系统会通知你。

    2. std::future

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

    2.1 应用场景

    • 异步任务:
      • 当我们需要在后台执行一些耗时操作时,如网络请求或者计算密集型任务时,std::future可以用来表示这些异步任务的结果。通过将任务与主线程分离,我们可以实现任务的并行处理,从而提高程序的执行效率。
    • 并发控制:
      • 在多线程编程中,我们可以需要等待某些任务完成后才能继续执行某些其他操作。通过使用std::future,我们可以实现线程间的同步,确保任务完成后再获取结果并继续执行后续操作。
    • 结果获取:
      • std::future提供了一种安全的方式来获取异步结果。我们可以使用std::future::get()函数来获取任务的结果,此函数会阻塞当前线程,直到异步任务完成。如此,在调用get()函数时,我们可以确保已经获取了所需的结果。
        举个更贴近实际开发的例子:
    • 开发一个网络请求模块:
      1. 异步任务:把网络请求(耗时)放到后台执行,主线程继续响应用户操作(比如刷新 UI);
      2. 并发控制:等用户点击 “提交” 按钮后,必须等网络请求完成(成功 / 失败),再提示用户 “提交成功 / 失败”;
      3. 结果获取:通过 future.get() 获取网络请求返回的状态码 / 数据,更新页面。

    3. 快速上手

    3.1 使用std::async关联异步任务

    std::async是一种将任务与std::future关联的简单方法。它创建并运行一个异步任务,并返回一个与该任务结果关联的std::future对象。默认情况下,std::async是否启动一个新线程,或者在等待future时,任务是否同步运行都取决于你的参数。这个参数为std::launch参数

    • std::launch::deferred表明该函数会被延迟调用,直到future上调用get()或者wait才会开始执行任务。
    • std::launch::async表明函数会在自己创建的线程上运行。
    • std::launch::deferred | std::launch::async内部通过系统等条件自动选择策略。
    #include <iostream>
    #include <thread>
    #include <future>
    #include <chrono>
    
    int get_num()
    {
        using namespace std::chrono_literals;
        std::this_thread::sleep_for(3s);
        std::cout<<"异步任务"<<std::endl;
        int num = 1;
        return num;
    }
    
    int main()
    {
        //1.关联异步任务    
        std::future<int> ret_future = std::async(std::launch::async,get_num);
        //2.执行其他操作
        std::cout<<"执行其他操作"<<std::endl;
        int ret = ret_future.get();
        std::cout<<"ret:"<<ret<<std::endl;
        return 0;
    }
    
    /*
    执行其他操作
    异步任务
    ret:1
    */
    

    3.2 使用std::packaged_task关联异步任务

    std::packaged_task就是将任务和std::future绑定在⼀起的模板,是⼀种对任务的封装。我们可以通过std::packaged_task对象获取任务相关联的std::future对象,通过调用get_future()⽅法获得。
    std::packaged_task的模板参数是函数签名。
    可以把std::future和std::async看成是分开的,而std::packaged_task则是⼀个整体。

    #include <iostream>
    #include <thread>
    #include <future>
    #include <chrono>
    
    int add(const int a,const int b)
    {
        return a+b;
    }
    
    int main()
    {
        //1.封装任务
        std::packaged_task<int(const int,const int)> task(add);
        //2.执行其他操作
        std::cout<<"执行其他操作"<<std::endl;
        std::future<int> ret_future = task.get_future();
        //3.同步执行任务,否则get获取future的值会一直阻塞。
        task(123,321);
        int ret = ret_future.get();
        std::cout<<"ret:"<<ret<<std::endl;
        return 0;
    }
    /*
    执行其他操作
    ret:444
    */
    

    异步执行std::package_task任务

    #include <iostream>
    #include <thread>
    #include <future>
    #include <chrono>
    #include <memory>
    
    int add(const int a, const int b)
    {
        using namespace std::chrono_literals;
        std::this_thread::sleep_for(3s);
        return a + b;
    }
    
    int main()
    {
        // 1.封装任务
        auto task = std::make_shared<std::packaged_task<int(int, int)>>(add);
    
        std::future<int> ret_future = task->get_future();
        std::thread th([task]()
                       { (*task)(123, 321); });
        
        // 2.执行其他操作
        std::cout << "执行其他操作" << std::endl;
        // 3.获取异步结果
        int ret = ret_future.get();
        std::cout << "ret:" << ret << std::endl;
        th.join();
        return 0;
    }
    /*
    执行其他操作
    ret:444
    */
    

    注意:
    std::packaged_task 是只可移动(Move-Only) 的类型,它禁止拷贝。因此,你不能像普通对象那样把它拷贝给 std::thread。有两种主要方式传递它:

    1. 使用 std::move():将 packaged_task 对象的所有权转移给新线程。但这样做之后,原 task 对象就变为空壳,不能再使用了。
    2. 使用智能指针(如 std::shared_ptr):就像你的例子一样,这是处理需要在多个地方共享(或生命周期不确定)的 packaged_task 的常用方法,尤其是在线程池等场景。

    3.3 使用std::promise关联异步任务

    std::promise提供了一种设置值的方式,它可以在设置之后通过相关性的std::future对象进行读取。换种说法就是之前说过的std::future可以读取一个异步函数的返回值,但是要等待就绪,而std::promise就提供了一种方式手动让std::future就绪。

    #include <iostream>
    #include <future>
    #include <thread>
    #include <chrono>
    
    void task(std::promise<int> ret_promise)
    {
        using namespace std::chrono_literals;
        int ret = 2;
        std::cout << "ret:" << ret << std::endl;
        std::this_thread::sleep_for(3s);
        ret_promise.set_value(ret);
    }
    
    int main()
    {
        // 1.创建promise
        std::promise<int> ret_promise;
        std::future<int> ret_future = ret_promise.get_future();
        // 创建新线程,执行长时间任务
        std::thread task_thread(task, std::move(ret_promise));
        // 2.执行其他操作
        std::cout << "执行其他操作" << std::endl;
        //3.获取异步结果
        int ret = ret_future.get();
        std::cout<<"ret:"<<ret<<std::endl;
        task_thread.join();
        return 0;
    }
    /* 
    执行其他操作
    ret:2 
    ret:2 
    */
    

    4. 三种异步操作的区别与应用场景

    1. std::async:最高层次的抽象

    • 核心思想:我有一个函数,想让它异步执行并获取其返回值。
    • 是什么:它是一个函数模板,像一个任务启动器。你给它一个函数和参数,它负责安排执行(可能创建新线程),并直接返回一个 std::future。
    • 耦合度:任务的创建、执行和 future 的关联是紧密耦合的。调用 std::async 一步到位。
    • 控制力:最低。你只能通过启动策略(async 或 deferred)进行有限的控制,无法决定任务具体在哪个线程上执行。
    • 应用场景:
      • “一次性”的异步调用:最常见的场景。当你只是想简单地将一个耗时操作(如文件读写、网络请求、复杂计算)放到后台,而不关心线程管理细节时,std::async 是最简单、最直接的选择。
      • 简单的并行计算:例如,将一个大任务分解成几个小任务,用多个 std::async 并行处理,最后收集结果。

    2. std::packaged_task:中等层次的抽象

    • 核心思想:我想把一个任务(可调用对象) 和它的未来结果(future) 打包在一起,但我自己来决定何时何地执行它。
    • 是什么:它是一个类模板,像一个任务包装器。它将一个函数或可调用对象封装起来,对外提供 get_future() 方法和一个函数调用操作符 operator()。
    • 耦合度:任务的定义与执行是解耦的。你可以先创建一个 packaged_task,获取它的 future,然后把这个 task 对象传递到任何地方(比如另一个线程、线程池的任务队列)去执行。
    • 控制力:中等。你可以精确控制任务在哪个线程上、在什么时候被调用。
    • 应用场景:
      • 线程池/任务队列:这是 packaged_task 的经典应用场景。主线程创建一堆 packaged_task,把它们塞进一个队列;工作线程从队列中取出任务并执行。主线程可以持有 future 来等待特定任务的结果。
      • 需要延迟或按需执行的任务:你可以先准备好一个任务包,但不立即执行,直到某个条件满足时再调用它。

    3. std::promise:最底层的抽象

    • 核心思想:我需要在两个独立执行的线程之间传递一个一次性的信号/值。一个线程是生产者(设置值),另一个是消费者(等待值)。
    • 是什么:它是一个类模板,像一个承诺。它将值的设置(promise)和值的获取(future)完全分离开。结果不一定来自函数返回值,可以是任何计算出的、接收到的或由事件触发的值。
    • 耦合度:结果的生产与消费是完全解耦的。生产者线程只需要持有 std::promise 对象,消费者线程只需要持有对应的 std::future 对象。它们之间甚至不需要知道对方的存在。
    • 控制力:最高。你可以手动在代码的任何位置,通过调用 promise.set_value() 或 promise.set_exception() 来让 future 就绪。
    • 应用场景:
      • 将回调风格的异步API转换为future风格:一个异步操作完成后会调用一个回调函数。你可以在发起异步操作前创建一个 promise,将 promise 传入回调函数(或其捕获列表),当回调被触发时,在回调函数内部调用 promise.set_value()。
      • 线程间的一次性事件通知:一个线程完成了某个阶段性工作,需要通知另一个正在等待的线程继续。
      • 生产者-消费者模型:当一个异步操作的结果不是通过 return 语句产生,而是通过一系列复杂的步骤计算出来时,可以使用 promise 在计算完成后“兑现承诺”。
    1 条回复 最后回复
    1
    • SPeakS 离线
      SPeakS 离线
      SPeak d2learn-dev mcpp-team
      编写于 最后由 编辑
      #2

      总结很好

      1 条回复 最后回复
      0

      • 登录

      • 没有帐号? 注册

      • 登录或注册以进行搜索。
      d2learn forums Powered by NodeBB
      • 第一个帖子
        最后一个帖子
      0
      • 版块
      • 最新
      • 标签
      • 热门
      • Online Tools
      • 用户
      • 群组