首页 > 后端开发 > C++ > 正文

如何在C++中执行异步任务_C++异步编程与std::async

裘德小鎮的故事
发布: 2025-09-26 18:30:02
原创
542人浏览过
std::async通过返回std::future实现异步任务执行,支持std::launch::async(强制新线程)和std::launch::deferred(延迟调用),避免默认策略的不确定性;其封装了线程管理、结果获取与异常传播,相比std::thread更简洁安全,适用于一次性任务,但需注意future生命周期导致的隐式阻塞及及时处理异常。

如何在c++中执行异步任务_c++异步编程与std::async

在C++中执行异步任务,核心思路是让某个操作在后台线程中独立运行,而当前线程可以继续执行其他工作,待需要结果时再获取。std::async是C++标准库提供的一个高级工具,它极大简化了异步编程的复杂性,允许你启动一个函数,并在未来某个时刻获取其结果,而无需直接管理线程。

解决方案

std::async提供了一种方便的方式来异步执行一个函数或可调用对象,并返回一个std::future对象,通过这个std::future,我们可以获取异步任务的返回值或者捕获可能发生的异常。

它的基本用法是这样的:

#include <iostream>
#include <future>
#include <thread>
#include <chrono>

// 一个模拟耗时操作的函数
int calculate_something(int input) {
    std::cout << "Task started with input: " << input << " on thread: " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时
    std::cout << "Task finished on thread: " << std::this_thread::get_id() << std::endl;
    return input * 2;
}

int main() {
    std::cout << "Main thread ID: " << std::this_thread::get_id() << std::endl;

    // 1. 使用默认策略(std::launch::async | std::launch::deferred)
    // 这种策略下,任务可能在新线程中运行,也可能在get()时同步运行。
    // 这也是一个常见的“陷阱”,因为行为不确定。
    std::future<int> future_result_default = std::async(calculate_something, 10);
    std::cout << "Task launched with default policy. Main thread continues..." << std::endl;
    // 此时主线程可以做其他事情...
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    std::cout << "Main thread doing something else." << std::endl;
    int result_default = future_result_default.get(); // 阻塞直到任务完成并获取结果
    std::cout << "Result from default policy: " << result_default << std::endl;

    std::cout << "------------------------------------" << std::endl;

    // 2. 明确指定 std::launch::async 策略
    // 强制任务在新线程中执行。这是大多数人期望的异步行为。
    std::future<int> future_result_async = std::async(std::launch::async, calculate_something, 20);
    std::cout << "Task launched with async policy. Main thread continues..." << std::endl;
    // 此时主线程可以做其他事情...
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    std::cout << "Main thread doing something else again." << std::endl;
    int result_async = future_result_async.get(); // 阻塞直到任务完成并获取结果
    std::cout << "Result from async policy: " << result_async << std::endl;

    std::cout << "------------------------------------" << std::endl;

    // 3. 明确指定 std::launch::deferred 策略
    // 任务不会立即执行,而是在future的get()或wait()方法被调用时,在调用线程中同步执行。
    // 这更像是“惰性求值”。
    std::future<int> future_result_deferred = std::async(std::launch::deferred, calculate_something, 30);
    std::cout << "Task launched with deferred policy. Main thread continues, but task is not running yet." << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    std::cout << "Main thread doing something else for deferred task." << std::endl;
    // 此时,deferred任务才真正开始执行
    int result_deferred = future_result_deferred.get();
    std::cout << "Result from deferred policy: " << result_deferred << std::endl;

    return 0;
}
登录后复制

这段代码展示了std::async的三种常见用法,尤其是通过std::launch策略控制任务的执行方式。get()方法会阻塞当前线程直到异步任务完成并返回结果。如果异步任务抛出异常,get()也会重新抛出该异常。

立即学习C++免费学习笔记(深入)”;

为什么我们需要异步任务?它解决了哪些痛点?

在我看来,异步任务的存在,很大程度上是为了解决“等待”的困境。在现代软件开发中,无论是桌面应用、服务器后端还是高性能计算,我们都不可避免地会遇到一些耗时操作。这些操作可能是I/O密集型(比如读写文件、网络请求),也可能是CPU密集型(比如复杂的计算、图像处理)。如果这些操作都按部就班地在主线程中执行,那么用户界面就会卡死,服务器响应会变慢,整个系统会显得非常迟钝。

具体来说,异步任务解决了以下几个核心痛点:

  1. 用户体验(UI响应性):这是最直观的。想象一下,你点击一个按钮,然后程序“假死”几秒钟,直到后台计算完成。这体验简直糟糕透顶。异步任务可以将耗时操作扔到后台线程,让主线程(尤其是UI线程)保持活跃,随时响应用户的输入,从而提升用户满意度。
  2. 资源利用率:现代处理器大多是多核的。如果你的程序只有一个线程,那么在执行CPU密集型任务时,其他核心就处于闲置状态,这无疑是巨大的浪费。异步任务允许你将不同的计算任务分发到不同的核心上并行执行,从而充分利用多核处理器的计算能力,显著缩短总的执行时间。对于I/O密集型任务,当一个线程在等待I/O完成时,另一个线程可以继续处理其他事情,避免了CPU空转。
  3. 系统吞吐量:在服务器端应用中,如果每个请求都同步处理,那么一个慢请求就会阻塞整个服务器,导致其他请求也无法及时响应。通过异步处理,服务器可以同时处理多个请求,当一个请求在等待数据库或网络响应时,可以切换到处理另一个请求,从而提高系统的并发处理能力和吞吐量。
  4. 简化复杂性(相对于裸线程):虽然std::thread提供了创建线程的能力,但要正确地管理线程的生命周期、传递参数、获取返回值、处理异常以及进行线程间同步,会涉及到std::promisestd::packaged_taskstd::mutexstd::condition_variable等一系列工具,这无疑增加了开发的复杂度和出错的风险。std::async在很多场景下,将这些底层细节抽象化,只通过一个std::future就解决了参数传递、结果获取和异常传播的问题,大大降低了异步编程的门槛。

对我而言,异步编程不仅仅是一种技术手段,它更像是一种思维模式的转变。它鼓励我们跳出线性的、顺序的思考框架,去思考如何将任务分解、并行,从而让程序更“活泼”,更具响应性。

std::async与std::thread有什么不同?何时选择它们?

std::asyncstd::thread都是C++中实现并发的工具,但它们的设计理念和适用场景有着显著的区别。理解这些差异,对于我们做出正确的选择至关重要。

std::thread:底层、精细控制

std::thread是C++标准库提供的最底层的线程抽象。当你使用std::thread时,你实际上是在直接操作一个操作系统级别的线程。

  • 特点
    • 直接控制:你可以完全控制线程的生命周期,包括创建、启动、分离(detach)或等待其完成(join)。
    • 无结果返回机制std::thread本身不提供直接的机制来获取线程函数的返回值。如果你需要返回值,通常需要配合std::promisestd::future,或者使用共享数据(如std::atomicstd::mutex保护的变量)。
    • 异常处理:线程函数内部抛出的异常不会自动传播到创建线程。你需要手动捕获并传递异常,通常也是通过std::promise
    • 强制管理:你必须明确调用join()detach()来管理线程的生命周期,否则在std::thread对象销毁时会调用std::terminate
  • 适用场景
    • 构建线程池:当你需要一个固定数量的线程来处理大量任务时,std::thread是构建线程池的基础。
    • 长期运行的服务:例如后台监听服务、日志记录器,这些线程通常需要一直运行,直到程序结束。
    • 对线程生命周期有精细控制需求:当你需要手动设置线程属性、优先级,或者需要线程间复杂的同步和通信时。
    • 学习和理解并发原语std::thread是理解C++并发模型的基础。

std::async:高层、任务导向

std::async是一个更高层次的抽象,它更侧重于“任务”而不是“线程”。它将线程的创建、管理、结果获取和异常传播等细节封装起来。

  • 特点
    • 任务导向:你给它一个可调用对象,它负责在后台执行,并返回一个std::future来代表未来的结果。
    • 自动管理std::async可以根据策略(std::launch::asyncstd::launch::deferred)自动决定是在新线程中运行任务,还是在调用get()wait()时同步运行。
    • 结果与异常:通过std::future,你可以轻松地获取任务的返回值,并且任务中抛出的异常也会被捕获并传播到get()调用处。
    • RAII风格std::future对象的析构函数会阻塞直到任务完成,这在一定程度上避免了std::threadjoindetach导致的std::terminate问题,但同时也可能带来意想不到的阻塞。
  • 适用场景
    • 一次性任务:当你只需要将一个函数或操作放到后台执行,并最终获取其结果时,std::async是理想选择。
    • 简化代码:对于简单的异步操作,std::async比手动使用std::thread配合std::promisestd::future要简洁得多。
    • 不需要精细控制线程:如果你不关心任务具体在哪一个线程上运行,也不需要对线程的生命周期进行复杂的管理。
    • 惰性求值:当使用std::launch::deferred策略时,可以实现类似函数式编程中的惰性求值。

何时选择?

我的经验是,优先考虑std::async。它更符合现代C++的RAII和抽象原则,能够有效减少并发编程的复杂性。只有当std::async的抽象无法满足你的需求时,例如:

豆包AI编程
豆包AI编程

豆包推出的AI编程助手

豆包AI编程 483
查看详情 豆包AI编程
  • 你需要一个长期运行的、可控的后台线程。
  • 你需要构建一个自定义的线程池。
  • 你需要对线程的调度、优先级、亲和性等有非常细致的控制。
  • 你正在处理一个非常底层的系统级并发问题。

这时,才应该考虑使用std::thread,并准备好处理所有相关的线程管理、同步和通信问题。简单来说,std::async是你的“日常工具”,而std::thread则是“高级定制工具”。

使用std::async时常见的陷阱和最佳实践

std::async虽然方便,但它也有一些容易让人掉坑的地方。理解这些,能帮助我们更稳健地编写异步代码。

常见的陷阱:

  1. 默认启动策略的非确定性行为

    • 这是std::async最常见的“坑”。当你像这样调用时:std::async(calculate_something, 10),你实际上是使用了默认的启动策略std::launch::async | std::launch::deferred。这意味着运行时系统可以自由选择是在一个新线程中异步执行任务,还是延迟到future_result.get()future_result.wait()被调用时才在当前线程同步执行。
    • 问题:这种不确定性可能导致你期望的并行性并未发生,或者在某些情况下,你可能遇到死锁(如果deferred任务试图获取一个已经被主线程持有的锁)。性能表现也可能因此变得难以预测。
    • 我的看法:如果你明确需要并行执行,总是明确指定std::launch::async。如果你需要惰性求值,就明确指定std::launch::deferred。不要依赖默认策略,它会给你带来很多困惑。
  2. std::future的生命周期导致的阻塞

    • 如果一个std::future对象被创建,但你没有调用它的get()wait()方法,并且这个std::future对象在任务完成之前被销毁了(例如,它是一个局部变量,函数返回了),那么std::future的析构函数会阻塞当前线程,直到它所关联的异步任务完成。
    • 问题:这可能会导致程序在不期望的地方阻塞,甚至影响性能。你可能认为任务在后台运行,但实际上因为future的销毁,主线程被强制等待。
    • 示例
      void run_async_task() {
          // future_obj是一个局部变量
          std::future<void> future_obj = std::async(std::launch::async, []{
              std::this_thread::sleep_for(std::chrono::seconds(5));
              std::cout << "Async task done." << std::endl;
          });
          std::cout << "Async task launched, future_obj will be destroyed soon." << std::endl;
          // future_obj 在这里析构,会阻塞当前线程5秒
      } // future_obj 离开作用域
      // main thread will block here for 5 seconds
      登录后复制
    • 解决方案:确保std::future的生命周期足够长,或者在不需要结果时,显式地调用wait()get()来处理任务的完成。如果不需要等待,可以考虑detach()一个std::thread,但那又回到了std::thread的复杂性。
  3. 过早调用get()wait()

    • 异步任务的意义在于“异步”,即主线程可以继续做其他事情。如果你在启动任务后立即调用future.get()future.wait(),那么主线程就会立即阻塞,等待异步任务完成,这实际上就变成了同步执行,失去了异步的优势。
    • 问题:这会抵消异步带来的性能提升和响应性改善。
    • 解决方案:只有当你确实需要异步任务的结果时,才调用get()。在此之前,让主线程处理其他可并行执行的任务。如果只是想检查任务是否完成而不阻塞,可以使用future.wait_for()future.wait_until()
  4. 异常处理的遗漏

    • 异步任务中抛出的异常会被存储在std::future中。如果你不调用get(),这个异常就永远不会被重新抛出,也不会被处理。
    • 问题:潜在的错误可能被掩盖,导致程序状态不一致或崩溃。
    • 解决方案总是get()调用处使用try-catch块来处理异步任务可能抛出的异常。

最佳实践:

  1. 明确指定启动策略:如前所述,如果你需要真正的并行执行,总是使用std::launch::async。如果你想要惰性求值,使用std::launch::deferred。避免默认策略带来的不确定性。

    std::future<int> fut = std::async(std::launch::async, my_function, args);
    登录后复制
  2. 合理管理std::future的生命周期:将std::future存储在适当的作用域或数据结构中,确保它在任务完成之前不会被意外销毁。如果任务的结果在程序的后期才需要,那么std::future可能需要作为成员变量或通过智能指针管理。

  3. 延迟get()调用:在启动异步任务后,让主线程尽可能地执行其他有用的工作,直到确实需要异步任务的结果时再调用get()。这最大化了并行执行的效益。

    std::future<int> fut = std::async(std::launch::async, complex_calculation);
    // 主线程执行其他独立的工作
    do_other_stuff();
    // 现在需要结果了
    int result = fut.get();
    登录后复制
  4. 利用wait_for()进行非阻塞等待:如果你不想阻塞主线程,但又想知道任务是否完成,可以使用std::future::wait_for()

    std::future<int> fut = std::async(std::launch::async, long_running_task);
    std::chrono::milliseconds span(100);
    while (fut.wait_for(span) == std::future_status::timeout) {
        std::cout << "Task not ready yet, doing something else..." << std::endl;
        // 主线程可以继续做其他事情
    }
    if (fut.valid()) {
        int result = fut.get();
        std::cout << "Task finished with result: " << result << std::endl;
    }
    登录后复制
  5. 妥善处理异常:在调用get()时,始终考虑并处理可能从异步任务中传播出来的异常。

    std::future<int> fut = std::async(std::launch::async, task_that_might_throw);
    try {
        int result = fut.get();
        std::cout << "Task completed successfully: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Task threw an exception: " << e.what() << std::endl
    登录后复制

以上就是如何在C++中执行异步任务_C++异步编程与std::async的详细内容,更多请关注php中文网其它相关文章!

编程速学教程(入门课程)
编程速学教程(入门课程)

编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号