【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索(一)

简介: 【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索

一、引言 (Introduction)

1.1 并发编程的概念 (Concept of Concurrent Programming)

并发编程是一种计算机编程技术,其核心在于使程序能够处理多个任务同时进行。在单核处理器上,虽然任何给定的时间只能运行一个任务,但通过任务切换,可以创建出并发执行的效果。而在多核处理器上,可以真正同时处理多个任务。

并发编程的目标是提高程序执行的效率。特别是在处理大量数据或需要进行大量计算的情况下,通过并发执行,可以显著提高程序的运行速度。同时,也可以提高程序的响应性,即使一部分任务在运行过程中出现阻塞,也不会影响到其他任务的执行。

并发编程并不是一项简单的任务,它涉及到许多复杂的问题,如数据同步、资源共享和死锁等。因此,需要深入理解并发编程的各种概念和技术,才能编写出正确和高效的并发程序。

C++作为一种广泛使用的编程语言,提供了一套完善的并发编程库。这些库包括多线程支持、同步工具(如互斥锁和条件变量)以及在本文中将深入探讨的std::future、std::async、std::packaged_task和std::promise等工具。

这些工具提供了强大的功能,可以帮助我们更方便地编写并发程序。然而,要充分利用它们,就需要深入理解它们的工作原理和使用方法。本文旨在帮助读者理解并掌握这些复杂但强大的工具。

1.2 C++并发编程的重要性 (Importance of Concurrent Programming in C++)

在当今的计算机环境中,处理器的核心数量正在迅速增长,使得并发编程变得越来越重要。对于许多应用程序来说,如果不能充分利用多核处理器的并行计算能力,那么它们就无法实现其潜在的性能。

并发编程在C++中具有特别重要的地位。C++是一种多范式编程语言,它支持过程式编程、面向对象编程,以及泛型编程等多种编程方式。并且,C++还提供了一套强大的并发编程库,使得我们可以编写出高效并且可扩展的并发程序。

C++并发编程的重要性体现在以下几个方面:

  • 性能提升:通过利用多核处理器的并行计算能力,我们可以编写出更高效的程序。在处理大量数据或需要进行大量计算的情况下,这种性能提升会非常明显。
  • 提高响应性:在许多应用程序中,我们需要在处理长时间运行的任务时,还能保持对用户输入的响应。通过并发编程,我们可以在一个线程中运行长时间的任务,而在另一个线程中处理用户输入,从而提高程序的响应性。
  • 利用现代硬件:随着处理器核心数量的增加,未来的程序必须能够处理并行和并发问题,以充分利用现代硬件的性能。并发编程正是解决这个问题的关键。
  • 编程模型的发展:并发编程是现代编程模型的一个重要组成部分。随着云计算、大数据和物联网等技术的发展,我们需要处理的数据量和计算任务越来越大,因此需要使用并发编程来满足这些需求。

综上所述,C++并发编程是任何C++程序员都应该掌握的重要技能。在本文中,我们将深入探讨C++的并发编程工具,包括std::future、std::async、std::packaged_task和std::promise,并通过实例来展示如何使用它们来编写高效的并发程序。

1.3 关于std::future、std::async、std::packaged_task和std::promise的简介 (Introduction to std::future, std::async, std::packaged_task, and std::promise)

在C++的并发编程中,std::future、std::async、std::packaged_task和std::promise是四个非常重要的工具。它们都是C++ 11并发编程库的一部分,并在C++ 14、17、20等更高版本中得到了进一步的优化和改进。下面,我们对这四个工具进行简单的介绍:

  • std::future:这是一个模板类,它代表一个异步操作的结果。这个异步操作可以是一个在另一个线程中运行的函数,或者是一个计算任务。std::future提供了一种机制,可以在结果准备好之后获取它,而不需要阻塞当前线程。
  • std::async:这是一个函数,它用于启动一个异步任务,并返回一个std::future对象,该对象将在将来保存这个任务的结果。std::async提供了一种简单的方式来运行异步任务,并获取其结果。
  • std::packaged_task:这是一个模板类,它封装了一个可调用对象(如函数或lambda表达式),并允许异步获取该对象的调用结果。当std::packaged_task对象被调用时,它会在内部执行封装的可调用对象,并将结果保存在一个std::future对象中。
  • std::promise:这是一个模板类,它提供了一种手动设置std::future对象的结果的方式。当你有一个异步任务,并且这个任务的结果需要在多个地方使用时,std::promise可以非常有用。

这四个工具提供了一种强大的并发编程模型,它允许我们将计算任务分配到多个线程中,然后在需要的时候获取这些任务的结果。在后续的章节中,我们将详细介绍这四个工具的工作原理和使用方法,并通过示例代码来展示如何在实际的程序中使用它们。

二、std::future:异步结果的储存与获取

2.1 std::future的基本原理和结构

在并发编程中,我们常常需要在多个线程之间传递数据。std::future是C++标准库中用于表示异步操作结果的类,它提供了一种非阻塞(或者说是异步)的方式来获取其他线程的计算结果。

基本原理

std::future的工作原理很简单:你创建一个std::future对象,然后把它传递给另一个线程,那个线程在某个时间点通过std::promisestd::packaged_task来设置结果。然后,你可以在任何时间点调用std::future::get()来获取结果。如果结果还没准备好,get()会阻塞当前线程直到结果可用。

std::future是一个模板类,它的模板参数是它所表示的异步操作的结果类型。例如,std::future表示一个异步操作,其结果是一个整数。

结构与方法

std::future主要包括以下几个方法:

  • get(): 获取异步操作的结果。这个操作会阻塞,直到结果准备好。这个方法只能调用一次,因为它会销毁内部的结果状态。
  • valid(): 检查是否有一个与此std::future关联的共享状态。如果有,返回true;否则,返回false。
  • wait(): 阻塞当前线程,直到异步操作完成。
  • wait_for(), wait_until(): 这两个函数可以用来设置超时,如果在指定的时间内结果还没准备好,它们就会返回。

下面是std::future的基本结构:

方法 描述
get() 获取异步操作的结果,如果结果还未准备好,会阻塞直到结果可用。
valid() 检查是否有一个与此std::future关联的共享状态。
wait() 阻塞当前线程,直到异步操作完成。
wait_for() 阻塞当前线程,直到异步操作完成或超过指定的等待时间。
wait_until() 阻塞当前线程,直到异步操作完成或到达指定的时间点。

理解了std::future的基本原理和结构,我们就可以开始探索它的实际应用了。在下一节中,我们将详细介绍std::future的使用场景和示例代码。

2.2 std::future的使用场景和示例代码

std::future 的主要使用场景是获取异步操作的结果。它通常与 std::async、std::packaged_task 或 std::promise 配合使用,以便在异步任务完成时获取结果。

使用 std::async 启动异步任务

在这种情况下,std::async 用于启动异步任务,并返回一个 std::future 对象,该对象可以用于获取异步任务的结果。以下是一个例子:

#include <future>
#include <iostream>
int compute() {
    // 假设这里有一些复杂的计算
    return 42;
}
int main() {
    std::future<int> fut = std::async(std::launch::async, compute);
    // 在这里我们可以做其他的事情
    int result = fut.get(); // 获取异步任务的结果
    std::cout << "The answer is " << result << std::endl;
    return 0;
}

使用 std::packaged_task 包装可调用对象

std::packaged_task 可以包装一个可调用对象,并允许你获取该对象的调用结果。以下是一个例子:

#include <future>
#include <iostream>
int compute() {
    // 假设这里有一些复杂的计算
    return 42;
}
int main() {
    std::packaged_task<int()> task(compute);
    std::future<int> fut = task.get_future();
    // 在另一个线程中执行任务
    std::thread(std::move(task)).detach();
    int result = fut.get(); // 获取异步任务的结果
    std::cout << "The answer is " << result << std::endl;
    return 0;
}

使用 std::promise 显式设置异步操作的结果

std::promise 提供了一个手动设置异步操作结果的方式。这在你需要更多控制或在异步操作不能直接返回结果的情况下非常有用。

#include <future>
#include <iostream>
#include <thread>
void compute(std::promise<int> prom) {
    // 假设这里有一些复杂的计算
    prom.set_value(42);
}
int main() {
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();
    // 在另一个线程中执行任务
    std::thread(compute, std::move(prom)).detach();
    int result = fut.get(); // 获取异步任务的结果
    std::cout << "The answer is " << result << std::endl;
    return 0;
}

以上就是 std::future 的主要使用场景和一些基本的示例代码。在下一节中,我们将探讨 std::future 在更高级的应用中的用法。

2.3 std::future在高级应用中的应用

std::future不仅可以用于简单的异步任务结果获取,更重要的是,它为编写复杂的并发和并行代码提供了基础。以下我们将介绍几个std::future在高级应用中的用法。

异步操作链

我们可以通过使用std::future和std::async创建异步操作链。在这个链中,一个操作的输出被用作下一个操作的输入,但这些操作可以在不同的线程上并发执行。

#include <future>
#include <iostream>
int multiply(int x) {
    return x * 2;
}
int add(int x, int y) {
    return x + y;
}
int main() {
    std::future<int> fut = std::async(std::launch::async, multiply, 21);
    
    // 启动另一个异步任务,该任务需要等待第一个异步任务的结果
    std::future<int> result = std::async(std::launch::async, add, fut.get(), 20);
    
    std::cout << "The answer is " << result.get() << std::endl;
    return 0;
}

异步数据流管道

我们还可以使用std::future创建异步数据流管道,其中每个阶段可以在不同的线程上并发执行。

#include <future>
#include <iostream>
#include <queue>
std::queue<std::future<int>> pipeline;
void stage1() {
    for (int i = 0; i < 10; ++i) {
        auto fut = std::async(std::launch::async, [](int x) { return x * 2; }, i);
        pipeline.push(std::move(fut));
    }
}
void stage2() {
    while (!pipeline.empty()) {
        auto fut = std::move(pipeline.front());
        pipeline.pop();
        int result = fut.get();
        std::cout << result << std::endl;
    }
}
int main() {
    std::thread producer(stage1);
    std::thread consumer(stage2);
    producer.join();
    consumer.join();
    return 0;
}

这些只是std::future在高级应用中的一些示例。实际上,std::future是C++并发编程中非常重要的一部分,它可以被用于构建各种复杂的并发和并行结构。

异步任务之间的依赖关系

当我们有一些任务需要按照特定的顺序执行时,我们可以使用std::future来实现这种依赖关系。

#include <future>
#include <iostream>
int task1() {
    // 假设这是一个耗时的任务
    return 42;
}
int task2(int x) {
    // 这个任务依赖于task1的结果
    return x * 2;
}
int main() {
    std::future<int> fut1 = std::async(std::launch::async, task1);
    std::future<int> fut2 = std::async(std::launch::async, task2, fut1.get());
    
    std::cout << "The answer is " << fut2.get() << std::endl;
    return 0;
}

在超时后取消异步任务

我们可以使用std::future::wait_for来实现在超时后取消异步任务的功能。

#include <future>
#include <iostream>
#include <chrono>
void task() {
    // 假设这是一个可能会超时的任务
    std::this_thread::sleep_for(std::chrono::seconds(5));
    std::cout << "Task completed" << std::endl;
}
int main() {
    std::future<void> fut = std::async(std::launch::async, task);
    
    std::future_status status = fut.wait_for(std::chrono::seconds(2));
    if (status == std::future_status::timeout) {
        std::cout << "Task cancelled due to timeout" << std::endl;
    } else {
        std::cout << "Task completed within timeout" << std::endl;
    }
    
    return 0;
}

注意,这个示例并不真正取消异步任务,而只是在任务超时后停止等待。真正的任务取消在C++中是一个更复杂的问题,需要使用其他的技术来实现。

这些示例只是展示了std::future的一部分用法,实际上std::future可以用于处理各种复杂的并发和并行问题。

三、std::async:异步任务的启动与管理 (std::async: Launching and Managing Asynchronous Tasks)

3.1 std::async的基本原理和结构 (Basic Principles and Structure of std::async)

C++11引入了std::async,这是一种非常方便的异步执行机制,可以让我们更容易地实现并发和并行操作。std::async可以立即启动一个异步任务,或者延迟执行,这取决于传递给它的启动策略。这个异步任务可以是函数、函数指针、函数对象、lambda表达式或者成员函数。

在C++中,std::async函数返回一个std::future对象,该对象可以用于获取异步任务的结果。在异步任务完成后,结果可以通过std::future::get()函数获取。如果结果还未准备好,这个调用会阻塞,直到结果准备就绪。

下面是std::async的基本原型:

template< class Function, class... Args >
std::future<std::invoke_result_t<std::decay_t<Function>, std::decay_t<Args>...>>
    async( Function&& f, Args&&... args );

这里的Function参数是异步任务,Args是传递给该任务的参数。std::async返回一个std::future,其模板参数是Function的返回类型。

std::async有两种模式:

  1. 异步模式 (std::launch::async):新的线程被立即启动并执行任务。
  2. 延迟模式 (std::launch::deferred):任务在future::get()future::wait()被调用时执行。

默认模式是std::launch::async | std::launch::deferred,即由系统决定是立即启动新线程,还是延迟执行。

std::async的主要优势在于,它允许你将对结果的需求(通过std::future)与任务的执行解耦,使得你能够更灵活地组织代码,而不必担心线程管理的细节。

3.2 std::async的使用场景和示例代码 (Use Cases and Example Code for std::async)

std::async是异步编程的有力工具,它可以处理各种场景,如:并行计算、后台任务、延迟计算等。

下面,我们将通过一些示例代码来演示std::async的使用。

3.2.1 基本的异步任务

这是一个简单的异步任务示例,我们在异步任务中计算斐波那契数列的一个元素:

#include <future>
#include <iostream>
int fibonacci(int n) {
    if (n < 3) return 1;
    return fibonacci(n-1) + fibonacci(n-2);
}
int main() {
    std::future<int> fut = std::async(fibonacci, 10);
    // 执行其他任务...
    int res = fut.get();  // 获取异步任务的结果
    std::cout << "The 10th Fibonacci number is " << res << "\n";
    return 0;
}

在上述代码中,我们创建了一个异步任务来计算斐波那契数列的第10个元素,然后继续执行其他任务。当我们需要斐波那契数的结果时,我们调用fut.get(),如果此时异步任务已经完成,我们就可以立即得到结果;如果异步任务还未完成,那么调用会阻塞,直到结果可用。

3.2.2 异步执行模式和延迟执行模式

std::async可以接受一个额外的参数,用于指定启动策略。这里是一个示例:

#include <future>
#include <iostream>
#include <thread>
void do_something() {
    std::cout << "Doing something...\n";
}
int main() {
    // 异步模式
    std::future<void> fut1 = std::async(std::launch::async, do_something);
    std::this_thread::sleep_for(std::chrono::seconds(1));  // 让主线程睡眠1秒
    fut1.get();
    // 延迟模式
    std::future<void> fut2 = std::async(std::launch::deferred, do_something);
    std::this_thread::sleep_for(std::chrono::seconds(1));  // 让主线程睡眠1秒
    fut2.get();
    return 0;
}

在上述代码中,我们首先以异步模式启动了一个任务,这个任务会立即在一个新线程中开始执行。然后,我们以延迟模式启动了另一个任务,这个任务会等到我们调用fut2.get()时才开始执行。

3.2.3 错误处理

如果异步任务中抛出了异常,那么这个异常会被传播到调用std::future::get()的地方。这使得错误处理变得简单:

#include <future>
#include <iostream>
int calculate() {
    throw std::runtime_error("Calculation error!");
     return 0;  // 这行代码永远不会被执行
}
int main() {
    std::future<int> fut = std::async(calculate);
    try {
        int res = fut.get();
    } catch (const std::exception& e) {
        std::cout << "Caught exception: " << e.what() << "\n";
    }
    return 0;
}

在上述代码中,异步任务calculate()抛出了一个std::runtime_error异常。这个异常被传播到了主线程,我们在主线程中捕获了这个异常,并打印了异常的消息。

std::async提供了一种简单、安全的方式来处理并发和并行编程,它将线程管理和结果获取的细节隐藏起来,让我们可以专注于实际的任务。


【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索(二)https://developer.aliyun.com/article/1464317

目录
相关文章
|
6天前
|
C++
C++11 std::lock_guard 互斥锁
C++11 std::lock_guard 互斥锁
12 0
|
6天前
|
前端开发 Go
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
【5月更文挑战第3天】Go语言通过goroutines和channels实现异步编程,虽无内置Future/Promise,但可借助其特性模拟。本文探讨了如何使用channel实现Future模式,提供了异步获取URL内容长度的示例,并警示了Channel泄漏、错误处理和并发控制等常见问题。为避免这些问题,建议显式关闭channel、使用context.Context、并发控制机制及有效传播错误。理解并应用这些技巧能提升Go语言异步编程的效率和健壮性。
30 5
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
|
6天前
|
C++
【C++】std::string 转换成非const类型 char* 的三种方法记录
【C++】std::string 转换成非const类型 char* 的三种方法记录
9 0
|
6天前
|
编译器 C语言 C++
【C++的奇迹之旅(二)】C++关键字&&命名空间使用的三种方式&&C++输入&输出&&命名空间std的使用惯例
【C++的奇迹之旅(二)】C++关键字&&命名空间使用的三种方式&&C++输入&输出&&命名空间std的使用惯例
|
6天前
|
安全 程序员 C++
【C++ 基本知识】现代C++内存管理:探究std::make_系列函数的力量
【C++ 基本知识】现代C++内存管理:探究std::make_系列函数的力量
109 0
|
6天前
|
设计模式 安全 算法
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
19 0
|
4天前
|
测试技术 C++
C++|运算符重载(3)|日期类的计算
C++|运算符重载(3)|日期类的计算
|
5天前
|
C语言 C++ 容器
C++ string类
C++ string类
9 0
|
6天前
|
C++ Linux