C++ 任务并行与异步编程

在这里插入图片描述

目录

任务并行与异步编程:探索 std::async

在现代软件开发中,有效地利用多核处理器的能力是提高应用性能的关键。C++11 提供了一系列工具,使得并行和异步编程变得触手可及,其中 std::async 是我们的好帮手。

想象一下,我们有一个复杂的数据分析任务或者需要加载大文件,这些操作可能会占用大量时间。在这种情况下,std::async 就像是一个神奇的工具箱,允许我们在后台轻松地执行这些任务,而不会阻塞我们的主程序流程。使用 std::async,我们可以像这样启动一个异步任务:

#include <future>
#include <iostream>

int longComputation() {
    // 假设这是一个耗时计算
    return 42; // 返回计算结果
}

int main() {
    // 启动异步任务
    auto result = std::async(longComputation);
    
    // 继续执行其他任务...
    std::cout << "计算进行中,请稍候...\n";
    
    // 当需要结果时,获取它
    std::cout << "结果: " << result.get() << std::endl;
    return 0;
}
灵活选择执行策略

std::async 不仅简化了任务的异步执行,还提供了灵活的执行策略。通过指定策略参数,我们可以控制任务的执行方式:

  • std::launch::async:这个策略指示程序尽可能立即在一个可用的线程上开始执行任务。这通常意味着在一个新的线程上。
  • std::launch::deferred:这个策略意味着任务将被延迟执行,直到我们首次请求其结果,通常是通过调用 std::future.get() 方法。任务将在当前线程中执行,而不是创建新线程。

这两种策略使得 std::async 可以灵活应对不同的程序设计场景,让开发者可以根据实际情况选择最合适的执行方式。

当你不指定任何策略(即使用默认参数调用 std::async),实际上相当于指定了 std::launch::async | std::launch::deferred。这意味着编译器和运行时环境有权自行决定使用哪种策略。它们可以基于当前的系统负载、线程可用性或其他运行时考虑因素来选择。通常:

  • 如果资源允许,任务可能会像使用 std::launch::async 一样立即在新线程上执行。
  • 如果系统资源紧张或者运行环境决定更合适,任务可能会延迟执行,直到你调用 get(),就像使用 std::launch::deferred

这种灵活性让 std::async 能适应不同的运行条件,但也增加了一些不确定性,因为你不能保证任务是否已经开始执行,除非明确指定执行策略。

需要说明的是,std::launch::async ,标准只要求任务必须异步执行,具体是否创建新线程,或者是否使用线程池中的线程,取决于C++运行时环境的实现细节。

在一些C++运行时环境中,为了效率和资源管理的考虑,可能会使用线程池来管理和复用线程。在这种情况下,当你使用 std::async 并指定 std::launch::async 策略时,任务可能会被分配给线程池中的一个现有线程来执行,而不是创建一个新的线程。

对于开发者来说,理解每种策略的具体行为非常重要,这样才能根据程序的需求做出恰当的选择。如果你需要确保任务立即执行,应明确使用 std::launch::async。如果你希望延迟执行或者想在需要结果之前不启动任务,std::launch::deferred 是个不错的选择。如果你希望让系统自行决定最佳策略,那么使用默认参数是合适的。

理解 std::future:未来的承诺

当我们使用 std::async 或其他并行工具启动一个异步任务时,它返回一个 std::future 对象。你可以将 std::future 想象成一个承诺:尽管结果还没准备好,但未来某时你一定能获取到它。

访问异步结果

使用 std::future 的优点是,我们可以在任务执行过程中继续做其他事情,而不必阻塞等待结果。只在你真正需要结果时,std::future 才会等待任务完成,保证结果的获取。这是通过 get() 方法实现的,它会阻塞当前线程,直到异步操作完成并返回结果。

处理异步中的异常

另一个重要特性是,如果异步操作中发生异常,std::future 会安全地捕获这些异常并保存下来。当你调用 get() 方法时,如果异步任务中抛出了异常,它会被重新抛出,允许你在主线程中处理异常:

try {
    auto riskyTask = std::async([]() -> int {
        throw std::runtime_error("有问题的任务!");
        return 0; // 这行代码实际上永远不会执行
    });
    int result = riskyTask.get(); // 这里将抛出异常
} catch (const std::exception& e) {
    std::cout << "捕获到异常: " << e.what() << std::endl;
}
std::promise 的关系

std::future 通常与 std::promise 配对使用,后者提供了一种方式来传递值或异常到 std::future。这种机制非常适合处理那些计算结果需要时间准备,但一旦准备好,就可以被其他线程安全访问的情况。

通过上面的介绍,我们看到 std::future 不仅提供了强大的异步结果管理能力,还增加了错误处理的灵活性,使我们能够编写更加健壮和高效的并行程序。接下来,如果你准备好了,我们可以一起看看 std::promise 是如何成为这个高效协作中不可或缺的一部分的。

std::promise:为未来承诺值

在 C++ 的异步编程中,std::promise 像是一个承诺者,它保证会在将来某个时刻提供一个计算结果或状态。它的存在让我们可以在一个线程中产生值,并安全地传递给另一个线程,这在处理复杂的并发场景时尤为重要。

std::promise 的主要用途是创建一个与 std::future 对象相关联的值。通过 std::promise,你可以在一个线程中设置值,并在另一个线程中通过 std::future 安全地访问这个值。

它们实现异步的关键在于如何分离值的设置和值的获取过程,使得这两个操作可以在不同的线程中发生。这里的“异步”不一定意味着任务必须在不同的线程上执行,而是指值的产生和消费可以不在同一时间发生。

std::promise 非常适合处理那些结果需要异步计算,并且计算完成后需要立即被其他线程知晓的场景。无论是在图形渲染、数据加载、或是网络通信中,std::promise 都能发挥巨大作用,提高程序的响应性和效率。

让我们通过一个例子来更清晰地理解它的作用:

#include <future>
#include <iostream>

void calculate(std::promise<int> promise) {
    try {
        // 执行一些计算
        int result = 527; // 模拟计算结果
        promise.set_value(result); // 将结果传递给 future
    } catch (...) {
        // 如果有异常,传递异常到 future
        promise.set_exception(std::current_exception());
    }
}

int main() {
    std::promise<int> promise;
    std::future<int> future = promise.get_future(); // 从 promise 获取 future
    
    std::thread producerThread(calculate, std::move(promise)); // 在新线程中执行计算

    try {
        std::cout << "计算结果: " << future.get() << std::endl; // 获取计算结果
    } catch (const std::exception& e) {
        std::cout << "异常: " << e.what() << std::endl;
    }

    producerThread.join(); // 等待线程结束
    return 0;
}

在这个例子中,std::promise 用于在一个线程中设置某个值,而 std::future 则用于在另一个线程中获取这个值。这个机制允许生产者线程和消费者线程解耦合,从而实现异步处理。这种情况下的“异步”主要是指生产者线程在未来某个不确定的时间点完成值的设置,而消费者线程则在需要时获取这个值,两者不需要同时进行。

另外在使用 std::promisestd::thread 的上下文中,使用 std::move 是非常重要的。这主要是因为 std::promise 对象不支持拷贝操作,只能被移动。这个设计是为了确保与 std::future 相关联的共享状态的唯一性和完整性。下面我们来探讨为什么必须使用 std::move

为什么使用 std::move

  1. 不支持拷贝std::promise 类不支持拷贝构造函数,只支持移动构造函数。这是因为每个 std::promise 对象都有一个独特的共享状态,这个状态与一个对应的 std::future 对象相关联。如果 std::promise 对象被拷贝,就会存在多个对同一个结果的引用,这可能导致对共享状态的管理变得复杂和出错。
  2. 保持状态的唯一性:通过移动 std::promise,你实际上是将原始对象的所有权和它的共享状态转移给新的对象。这样做可以保证共享状态的唯一性,确保只有一个 std::promise 对象可以设置值或异常,同时只有一个 std::future 可以访问这个值。
  3. 线程安全:当你在多线程环境下工作时,确保每个线程中只有一个唯一的 std::promise 对象可以访问特定的共享状态是很重要的。使用 std::move 可以确保共享状态在不同线程间正确地、安全地转移,避免了潜在的数据竞争或同步问题。

std::future, std::promise, 和 std::async 的关系及其合作方式

std::futurestd::promisestd::async 是处理异步编程的重要工具,它们通过不同的方式协同工作,以适应各种场景的需求:

  1. std::async:
    • std::async 是一个函数模板,用于简化异步任务的创建和管理。调用 std::async 时,它可以启动一个异步任务(可能在新线程中,或使用 std::launch::deferred 策略延迟执行),并立即返回一个 std::future 对象。这个 std::future 对象用于获取异步任务的结果。
    • 使用 std::async 的关键好处是自动处理线程的创建和管理,省去了直接与线程交互的需要。当与 std::launch::deferred 策略结合使用时,任务将延迟到 std::futureget()wait() 被调用时在同一线程中执行,这适用于不急于立即执行或不需要多线程处理的任务。
  2. std::promise:
    • std::promise 提供了一种手动设置值或异常的方式,在任何时点都可以设定,并通过与之关联的 std::future 对象让其他线程能够安全地获取这个值或异常。
    • std::promise 适用于需要精确控制值何时被设置的更复杂或需要显式线程管理的场景,它支持在多个地点设置和获取未来值,特别适合生产者-消费者模型,其中生产者通过 std::promise 提供数据,而消费者通过 std::future 接收数据。
  3. std::future:
    • std::future 作为一个从异步操作获取结果的接口,既可以与 std::async 直接配合使用,也可以通过 std::promise 来设置值。
    • 它为获取异步或手动设置的结果提供了统一的接口,连接了 std::asyncstd::promise 的功能,使得异步编程更为灵活和高效。
  • 32
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

泡沫o0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值