一、引言 (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::promise
或std::packaged_task
来设置结果。然后,你可以在任何时间点调用std::future::get()
来获取结果。如果结果还没准备好,get()
会阻塞当前线程直到结果可用。
std::future
是一个模板类,它的模板参数是它所表示的异步操作的结果类型。例如,std::future<int>
表示一个异步操作,其结果是一个整数。
结构与方法
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
有两种模式:
- 异步模式 (
std::launch::async
):新的线程被立即启动并执行任务。 - 延迟模式 (
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
提供了一种简单、安全的方式来处理并发和并行编程,它将线程管理和结果获取的细节隐藏起来,让我们可以专注于实际的任务。
3.3 std::async在高级应用中的应用 (Applications of std::async in Advanced Use Cases)
std::async
不仅仅能用于简单的异步任务,还可以在一些高级的应用场景中发挥作用。这些应用通常涉及到大量的计算或者需要并行处理的场景。
3.3.1 并行算法
在需要处理大量数据的情况下,我们可以使用std::async
来并行化算法。例如,假设我们需要对一个数组进行排序,我们可以将数组分成两半,然后在两个异步任务中分别排序这两半,最后再合并结果。
这是一个并行排序的示例:
#include <algorithm>
#include <future>
#include <vector>
template <typename T>
void parallel_sort(std::vector<T>& v) {
if (v.size() <= 10000) { // 对于小数组,直接排序
std::sort(v.begin(), v.end());
} else { // 对于大数组,分成两半并行排序
std::vector<T> v1(v.begin(), v.begin() + v.size() / 2);
std::vector<T> v2(v.begin() + v.size() / 2, v.end());
std::future<void> fut = std::async([&v1] { parallel_sort(v1); });
parallel_sort(v2);
fut.get();
std::merge(v1.begin(), v1.end(), v2.begin(), v2.end(), v.begin());
}
}
在这个示例中,我们定义了一个并行排序函数parallel_sort
。如果数组的大小小于10000,我们直接对数组进行排序;如果数组的大小大于10000,我们将数组分成两半,然后在一个异步任务中排序第一半,在主线程中排序第二半,最后合并结果。
3.3.2 后台任务
在一些情况下,我们可能需要在后台执行一些任务,这些任务可能需要很长时间才能完成。例如,我们可能需要在后台下载一个文件,或者执行一些复杂的计算。std::async
提供了一种简单的方式来处理这种情况。
下面是一个在后台下载文件的示例:
#include <future>
#include <iostream>
#include <string>
std::string download_file(const std::string& url) {
// 用你的下载库下载文件...
return "file content";
}
int main() {
std::future<std::string> fut = std::async(download_file, "http://example.com/file");
// 在此处执行其他任务...
std::string file_content = fut.get();
std::cout << "Downloaded file content: " << file_content << "\n";
return 0;
}
在这个示例中,我们在一个异步任务中下载一个文件,然后继续执行其他任务。当我们需要文件的内容时,我们调用fut.get()
获取结果。
3.3.3 异步日志系统
在许多系统中,日志系统是一个关键的组件,用于记录程序的运行情况。然而,写入日志可能是一个时间开销很大的操作,特别是当我们需要写入大量日志时。使用std::async
,我们可以将日志写入操作放在一个单独的线程中,从而避免阻塞主线程。
下面是一个异步日志系统的简单示例:
#include <future>
#include <iostream>
#include <string>
#include <vector>
void write_log(const std::string& log) {
// 在这里写入日志,例如:
std::cout << "Writing log: " << log << std::endl;
}
std::future<void> log_async(const std::string& log) {
return std::async(std::launch::async, write_log, log);
}
int main() {
std::vector<std::future<void>> futures;
futures.push_back(log_async("Start program"));
// 执行其他任务...
futures.push_back(log_async("End program"));
// 等待所有异步日志任务完成
for (auto& future : futures) {
future.get();
}
return 0;
}
在这个示例中,我们在一个异步任务中写入日志,然后立即返回,不等待日志写入完成。这样,我们就可以在不阻塞主线程的情况下写入日志。
3.3.4 实时计算系统
在一些实时计算系统中,我们可能需要在一定的时间内完成一些任务,否则就需要中止这些任务。std::async
和std::future
提供了一种简单的方式来实现这种需求。
下面是一个实时计算系统的示例:
#include <future>
#include <iostream>
#include <chrono>
int calculate() {
// 在这里执行一些复杂的计算...
return 42;
}
int main() {
std::future<int> fut = std::async(std::launch::async, calculate);
std::chrono::milliseconds span(100); // 最多等待100毫秒
if (fut.wait_for(span) == std::future_status::ready) {
int result = fut.get();
std::cout << "Result is " << result << "\n";
} else {
std::cout << "Calculation did not finish in time\n";
}
return 0;
}
在这个示例中,我们在一个异步任务中执行计算,然后等待最多100毫秒。如果计算在这个时间内完成,我们就获取结果;否则,我们就打印一条消息,表示计算没有在时间内完成。
四、std::packaged_task:封装可调用目标的功能
4.1 std::packaged_task的基本原理和结构
std::packaged_task
是C++11引入的一种工具,它的主要作用是封装可调用的对象,如函数、lambda表达式、函数指针或函数对象,这使得我们可以在不同的上下文或线程中执行这些任务。std::packaged_task
对异步操作进行抽象,可以将其视为一个“包裹”,其中包含了异步操作的所有必要信息。
基本原理
在std::packaged_task
的内部,其将所封装的可调用对象和一个std::future
对象关联在一起。当我们调用std::packaged_task
对象时,它会执行所封装的任务,然后将结果存储在std::future
中。这样,我们就可以通过这个std::future
来获取任务的结果,无论任务是在哪个线程中完成的。
// 创建一个 packaged_task,它将 std::plus<int>() 封装起来
std::packaged_task<int(int, int)> task(std::plus<int>());
// 获取与 task 关联的 future
std::future<int> result_future = task.get_future();
// 在另一个线程中执行 task
std::thread(std::move(task), 5, 10).detach();
// 在原线程中,我们可以从 future 中获取结果
int result = result_future.get(); // result == 15
结构
std::packaged_task
是一个模板类,其模板参数是可调用对象的类型。例如,如果我们有一个返回void
并接受一个int
参数的函数,那么我们可以创建一个std::packaged_task<void(int)>
的对象。
std::packaged_task
主要包含以下几个公有成员函数:
- 构造函数:用于构造
std::packaged_task
对象,并将可调用对象封装在内部。 -
operator()
: 用于调用封装的任务。 -
valid()
: 用于检查std::packaged_task
是否含有一个封装的任务。 -
get_future()
: 用于获取与std::packaged_task
关联的std::future
对象。 -
swap()
: 用于交换两个std::packaged_task
对象的内容。
通过合理地使用std::packaged_task
,我们可以更好地管理异步任务,并从任何地方获取任务的结果。在C++并发编程中,这是一种非常有用的工具。
4.2 std::packaged_task的使用场景和示例代码
std::packaged_task
在多线程编程中有广泛的应用,主要适用于那些需要异步执行任务并获取结果的场景。下面是几个使用std::packaged_task
的典型场景:
-
异步任务执行:当你需要在另一个线程中执行任务,并且希望在当前线程中获取结果时,你可以使用
std::packaged_task
。 -
任务队列:你可以创建一个
std::packaged_task
的队列,将任务放入队列中,并由一个或多个工作线程来执行这些任务。 -
Future/Promise模型:你可以使用
std::packaged_task
实现Future/Promise模型,其中std::future
用于获取结果,std::packaged_task
用于执行任务并存储结果。
以下是一个std::packaged_task
的使用示例:
#include <iostream>
#include <future>
#include <thread>
// 一个要在子线程中执行的函数
int calculate(int x, int y) {
return x + y;
}
int main() {
// 创建一个packaged_task,将calculate函数封装起来
std::packaged_task<int(int, int)> task(calculate);
// 获取与task关联的future
std::future<int> result = task.get_future();
// 创建一个新线程并执行task
std::thread task_thread(std::move(task), 5, 10);
// 在主线程中,我们可以从future中获取结果
int result_value = result.get();
std::cout << "Result: " << result_value << std::endl; // 输出: Result: 15
task_thread.join();
return 0;
}
在这个例子中,我们创建了一个std::packaged_task
对象task
,它将calculate
函数封装起来。然后我们在一个新的线程中执行task
,并在主线程中通过std::future
获取结果。这样我们就能够异步地执行任务,并在需要的时候获取结果。
4.3 std::packaged_task在高级应用中的应用
std::packaged_task
在复杂的多线程环境中有很多高级应用,比如任务队列、线程池和异步任务链等。以下将简要介绍几个应用案例。
任务队列
任务队列是一种常见的多线程设计模式,允许多个生产者线程提交任务,然后由一个或多个消费者线程执行这些任务。std::packaged_task
非常适合用来实现任务队列,因为它可以将任意的可调用对象封装成一个统一的接口。
#include <queue>
#include <future>
#include <mutex>
// 任务队列
std::queue<std::packaged_task<int()>> tasks;
std::mutex tasks_mutex;
// 生产者线程
void producer() {
// 创建一个packaged_task
std::packaged_task<int()> task([]() { return 7 * 7; });
// 将task添加到任务队列中
std::lock_guard<std::mutex> lock(tasks_mutex);
tasks.push(std::move(task));
}
// 消费者线程
void consumer() {
// 从任务队列中取出一个task并执行
std::lock_guard<std::mutex> lock(tasks_mutex);
if (!tasks.empty()) {
std::packaged_task<int()> task = std::move(tasks.front());
tasks.pop();
task();
}
}
线程池
线程池是一种常见的多线程设计模式,它创建一定数量的线程,并复用这些线程来执行任务。std::packaged_task
可以用来实现线程池中的任务,因为它可以在一个线程中执行任务,并在另一个线程中获取结果。
异步任务链
异步任务链是一种设计模式,其中一个任务的结果被用作下一个任务的输入。std::packaged_task
可以用来实现异步任务链,因为它可以在任务完成时将结果存储在std::future
中,然后这个std::future
可以被下一个任务用来获取结果。
// 第一个任务
std::packaged_task<int()> task1([]() { return 7 * 7; });
std::future<int> future1 = task1.get_future();
// 第二个任务,它的输入是第一个任务的结果
std::packaged_task<int(int)> task2([](int x) { return x + 1; });
std::future<int> future2 = task2.get_future();
// 在一个线程中执行第一个任务
std::thread(std::move(task1)).detach();
// 在另一个线程中执行第二个任务
std::thread([&]() {
task2(future1.get());
}).detach();
// 获取第二个任务的结果
int result = future2.get();
在这个例子中,我们创建了一个异步任务链,其中第一个任务计算7 * 7
,然后第二个任务将结果加一。我们在两个不同的线程中执行这两个任务,然
后在主线程中获取最终的结果。这展示了std::packaged_task
在高级并发编程中的强大能力。
特性\模型 | 任务队列 | 线程池 | 异步任务链 |
---|---|---|---|
适用场景 | 需要在多个线程中分发和执行任务,适合生产者-消费者模型 | 需要优化任务执行性能,避免频繁创建和销毁线程,适合并发高、任务量大的场景 | 任务之间存在依赖关系,下游任务需要使用上游任务的结果,适合数据处理和计算密集型任务 |
资源使用 | 可根据任务队列的长度动态调整线程数量,资源使用灵活 | 固定数量的线程,提前创建并复用,资源使用稳定 | 每个任务可能在不同的线程中执行,资源使用灵活,但可能需要更多的线程间同步 |
任务管理 | 任务通过队列进行管理,可以按照先进先出或优先级等策略调度任务 | 任务通常由线程池内部的任务队列进行管理,线程池负责任务的调度和执行 | 任务的管理需要根据任务之间的依赖关系进行,通常需要更复杂的逻辑 |
结果获取 | 通过std::future 获取结果,可以异步获取,或阻塞等待结果 |
通过std::future 获取结果,可以异步获取,或阻塞等待结果 |
通过std::future 获取结果,可以异步获取,或阻塞等待结果,下游任务可以直接使用上游任务的结果 |
错误处理 | 错误通常需要在执行任务的线程中捕获,并通过std::future 传递给获取结果的线程 |
错误通常需要在执行任务的线程中捕获,并通过std::future 传递给获取结果的线程 |
错误通常需要在执行任务的线程中捕获,并通过std::future 传递给获取结果的线程,错误可能会导致整个任务链的中断 |
五、std::promise:异步操作的结果承诺 (std::promise: Promise of Asynchronous Operation Results)
5.1 std::promise的基本原理和结构 (Basic Principles and Structure of std::promise)
std::promise 是一个在 C++11 及其后续版本中被引入的并发编程工具,它允许我们在一个线程中设置一个值或异常,然后在另一个线程中获取这个值或异常。这样的特性使得 std::promise 成为了一种强大的线程间通信手段。
设想一下,你正在组织一场盛大的晚会,而你需要为你的客人承诺他们会得到美味的食物。在这种情况下,你可能会雇佣一位厨师来准备食物。你向客人承诺(promise)将会有美食,而厨师则在背后工作,尽力满足你的承诺。在 C++ 中,这个过程就像一个线程(厨师)在工作,而另一个线程(你)在等待结果。
现在,让我们深入 std::promise 的底层原理和结构。
基本原理
std::promise 的基本原理很简单。当你创建一个 std::promise 对象时,你可以给它一个值或者一个异常。这个值或者异常可以被一个与该 promise 关联的 std::future 对象获取。这就是一个标准的“生产者-消费者”模型,在这个模型中,promise 是生产者,而 future 是消费者。
结构
std::promise 是一个模板类,它有一个模板参数 T,表示承诺的值的类型。一个 std::promise 对象可以通过它的成员函数 set_value 来设置一个值,或者通过成员函数 set_exception 来设置一个异常。这些值或异常可以通过与之关联的 std::future 对象来获取。
下面是一个 std::promise 的简单使用示例:
#include <iostream>
#include <future>
#include <thread>
void my_promise(std::promise<int>& p) {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟一些工作
p.set_value(42); // 设置值
}
int main() {
std::promise<int> p;
std::future<int> f = p.get_future(); // 获取 future
std::thread t(my_promise, std::ref(p)); // 在新线程中运行函数
std::cout << "Waiting for the answer...\n";
std::cout << "The answer is " << f.get() << '\n'; // 获取值
t.join();
return 0;
}
这个示例中,my_promise 函数在一个新线程中运行,并设置 promise 的值为 42。主线程等待future 的值,然后打印出来。注意到,主线程会阻塞在 f.get() 处,直到 promise 的值被设置。
在下一节中,我们将详细介绍 std::promise 的使用场景和示例代码。
5.2 std::promise的使用场景和示例代码 (Use Cases and Example Code for std::promise)
使用场景
std::promise 最常见的使用场景是在多线程环境中进行线程间通信,尤其是当你需要在一个线程中设置一个值(或者一个异常),并在另一个线程中获取这个值(或异常)时。
此外,std::promise 可以用于以下场景:
- 异步任务:当你需要运行一个可能会花费很长时间的任务,并且你不想等待这个任务完成,你可以使用 std::promise 在一个新线程中运行这个任务,并在主线程中获取结果。
- 数据流管道:你可以使用一系列的 std::promise 和 std::future 对象来创建一个数据流管道,其中每个线程都是管道的一部分,并且每个线程都通过 std::promise 对象提供数据,然后通过 std::future 对象获取数据。
示例代码
让我们通过一个例子来展示如何使用 std::promise。在这个例子中,我们将使用一个 promise 来传递一个从新线程中计算出来的结果。
#include <iostream>
#include <future>
#include <thread>
// 这个函数将会在一个新线程中被运行
void compute(std::promise<int>& p) {
int result = 0;
// 做一些计算...
for (int i = 0; i < 1000000; ++i) {
result += i;
}
// 计算完成,设置 promise 的值
p.set_value(result);
}
int main() {
// 创建一个 promise 对象
std::promise<int> p;
// 获取与 promise 关联的 future 对象
std::future<int> f = p.get_future();
// 在新线程中运行 compute 函数
std::thread t(compute, std::ref(p));
// 在主线程中获取结果
std::cout << "The result is " << f.get() << std::endl;
// 等待新线程完成
t.join();
return 0;
}
在这个例子中,我们在新线程中运行了一个可能会花费很长时间的计算任务,并使用了一个 promise 来传递计算结果。在主线程中,我们通过 future 对象获取了这个结果。当我们调用 f.get()
时,主线程会阻塞,直到新线程完成计算并设置 promise 的值。
5.3 std::promise在高级应用中的应用 (Applications of std::promise in Advanced Use Cases)
std::promise 不仅仅可以在基础的多线程编程中使用,它也有一些高级应用场景,比如与其它并发工具结合使用以提高程序的性能和效率。以下是两个使用 std::promise 的高级应用场景:
高级应用一:链式异步任务
在某些情况下,你可能需要执行一系列的异步任务,其中每个任务的输入都依赖于前一个任务的输出。这种情况下,你可以创建一个 promise 和 future 的链,每个任务都有一个输入 future 和一个输出 promise,这样就可以确保任务的执行顺序,并且可以方便地获取每个任务的结果。
例如,下面的代码展示了如何使用 promise 和 future 的链来执行一系列的异步任务:
#include <iostream>
#include <future>
#include <thread>
void chain_task(std::future<int>& f, std::promise<int>& p) {
int input = f.get(); // 获取输入
int output = input * 2; // 执行一些计算
p.set_value(output); // 设置输出
}
int main() {
// 创建 promise 和 future 的链
std::promise<int> p1;
std::future<int> f1 = p1.get_future();
std::promise<int> p2;
std::future<int> f2 = p2.get_future();
// 在新线程中运行异步任务
std::thread t1(chain_task, std::ref(f1), std::ref(p2));
std::thread t2(chain_task, std::ref(f2), std::ref(p1));
// 设置初始输入
p1.set_value(42);
// 获取最终结果
std::cout << "The final result is " << f1.get() << std::endl;
// 等待新线程完成
t1.join();
t2.join();
return 0;
}
高级应用二:与其它并发工具结合使用
std::promise 可以与 C++ 标准库中的其它并发工具结合使用,比如 std::async、std::packaged_task、std::thread 等,来创建更复杂的并发模式。
例如,你可以使用 std::async 来启动一个异步任务,并使用 std::promise 来传递任务的结果。你也可以使用 std::packaged_task 来封装一个可以在新线程中运行的任务,并使用 std::promise 来设置任务的结果。在这种情况下,std::promise 可以提供更高级的线程间通信机制,使你可以在不同的线程中共享数据和状态。
以上就是 std::promise 在高级应用中的一些使用场景,希望可以帮助你更好地理解并使用这个工具。在下一章中,我们将介绍 std::future、std::async、std::packaged_task 和 std::promise 的比较和选择。
六、并行类和线程池
并行库
std::future
是C++标准库的一部分,它表示将来可能在其他线程上计算出的一个值。std::future
本身并不直接涉及线程池。然而,它通常与如std::async
等机制结合使用,这些机制可以利用线程池执行异步任务。
事实上,std::async
的行为取决于给它的参数。如果传入参数 std::launch::async
,它将在新线程中执行任务。如果传入参数 std::launch::deferred
,任务将在调用 std::future::get()
时同步运行。无论如何,std::async
的实现可以使用线程池,这取决于标准库的实现和系统限制。
总之,std::future
并不直接与线程池有关,但它可以与使用线程池的异步执行机制一起使用。
C++标准库中,并没有直接提供线程池功能。std::future
和 std::async
只提供了一种基本的异步执行方式,所以在C++标准库中,你无法直接控制线程池的细节,例如工作线程数量、可调参数等。要实现这种控制,你可以创建自定义线程池,或使用已有的开源线程池库。
std::packaged_task
也可以与线程池一起使用,但它本身并不是一个线程池实现。std::packaged_task
是一个在C++中包装可调用对象的类模板,它允许将函数与一个 std::future
结合使用。当该可调用对象(函数、lambda表达式或函数对象)被调用时,std::packaged_task
会将结果存储起来,并使关联的 std::future
变得就绪。
你可以使用 std::packaged_task
创建任务,然后将这些任务提交给一个线程池。这使得在线程池中执行的任务能够返回一个 std::future
对象,从而对任务结果进行异步访问。
选择权衡
线程池通常更适用于长时间运行的任务,因为线程池意味着在执行时间较长的任务时可以复用线程资源。这样就能避免频繁地创建和销毁线程所带来的性能损失。线程池还允许你控制并发线程的数量来满足特定性能需求或系统限制。
而对于短时间且不频繁的任务,使用并行库(如C++标准库中的 std::async
、Intel TBB、Microsoft PPL和C++ Boost.Asio库)可能更恰当。这些库在只需执行少量任务时可以提供简便的接口,并避免为管理线程池带来额外的复杂性。并行库通常会处理线程创建和销毁的资源管理问题,因此对于这些罕见的任务是一个不错的选择。
请注意,在具体选择如何并发执行任务时,任务的性质(如任务是否有优先级、是否需要同步之类)以及所使用的库(它们会有不同的功能和优化)也是应该考虑的因素。
自定义线程池
以下是一个简单的自定义线程池示例:
#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
class ThreadPool {
public:
ThreadPool(size_t num_threads);
~ThreadPool();
void enqueue(std::function<void()> task);
private:
void worker();
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex tasks_mutex;
std::condition_variable tasks_cv;
bool stop;
};
ThreadPool::ThreadPool(size_t num_threads) : stop(false) {
for (size_t i = 0; i < num_threads; ++i) {
workers.emplace_back(&ThreadPool::worker, this);
}
}
ThreadPool::~ThreadPool() {
{
std::unique_lock<std::mutex> lock(tasks_mutex);
stop = true;
}
tasks_cv.notify_all();
for (auto &worker : workers) {
worker.join();
}
}
void ThreadPool::enqueue(std::function<void()> task) {
{
std::unique_lock<std::mutex> lock(tasks_mutex);
tasks.push(task);
}
tasks_cv.notify_one();
}
void ThreadPool::worker() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(tasks_mutex);
tasks_cv.wait(lock, [this]() { return !tasks.empty() || stop; });
if (stop && tasks.empty()) {
return;
}
task = tasks.front();
tasks.pop();
}
task();
}
}
通过以上自定义线程池实现,你可以自由地控制线程池的大小,以及对任务队列进行管理。此外,还有许多开源线程池库可供选择,例如 Intel TBB,Microsoft PPL和C++ Boost.Asio库。这些库为多线程编程提供了更多的优化和高级控制。
并行库对线程池的帮助
C++中的并行类,包括std::thread、std::future、std::async、std::packaged_task和std::promise等,可以用来实现线程池,这对于提高多核处理器的利用率,减少线程创建和销毁的开销,以及提高程序的响应性能具有重要的帮助。下面我们详细讨论这些类如何辅助实现线程池。
1. std::thread
std::thread 是 C++ 的线程库中的基础,它可以用来创建和管理线程。在实现线程池时,我们通常会创建一组线程并保存在容器中(例如std::vector)。这些线程在创建时会开始执行一个特定的函数,这个函数通常是一个无限循环,不断从任务队列中取出任务并执行。
2. std::future和std::promise
std::future 和 std::promise 可以用来传递和获取任务的结果。在实现线程池时,我们通常会为每个任务创建一个 std::promise 对象,并将对应的 std::future 对象返回给调用者。当任务完成时,工作线程将结果设置到 std::promise 对象中,调用者可以通过 std::future 对象获取结果。
3. std::async
std::async 是一种简单的异步编程工具,它可以用来启动一个异步任务并返回一个 std::future 对象。虽然 std::async 本身并不适合用来实现线程池(因为它总是创建新的线程),但是我们可以借鉴它的设计来简化线程池的接口。具体来说,我们可以提供一个类似于 std::async 的函数,这个函数接受一个可调用对象和一组参数,将它们封装成任务并添加到任务队列中,然后返回一个 std::future 对象。
4. std::packaged_task
std::packaged_task 可以看作是一个包装了可调用对象的类,它将可调用对象和一个 std::promise 对象绑定在一起。当调用 std::packaged_task 对象时,它会调用内部的可调用对象,并将结果保存到 std::promise 对象中。在实现线程池时,我们可以用 std::packaged_task 来封装任务,这样就可以将任何可调用对象转换为一个可以放入任务队列的统一类型。
这些并行类提供了创建线程、异步执行任务和传递任务结果等基础功能,使得我们可以在 C++ 中实现高效的线程池。而线程池的使用可以更好地控制线程的数量,避免过多的线程创建和销毁带来的开销,提高多核处理器的利用率,从而提高程序的性能。
类名 | 功能描述 | 实现线程池的作用 | 用户编程的角度 | 实用性 |
---|---|---|---|---|
std::thread | 用于创建和管理线程 | 线程池的基础,负责执行任务 | 简单易用,但需要手动管理线程生命周期 | 高 |
std::future | 用于获取异步任务的结果 | 提供任务结果的获取方式,使调用者可以等待任务完成并获取结果 | 提供了一种安全且简单的方式来获取异步任务的结果 | 高 |
std::promise | 用于设置异步任务的结果 | 提供任务结果的设置方式,使工作线程可以设置任务的结果 | 需要与 std::future 配合使用,使用稍复杂 | 中 |
std::async | 用于启动异步任务 | 可以借鉴其设计来简化线程池的接口 | 非常简单易用,但不适合用于实现线程池 | 中 |
std::packaged_task | 用于封装任务 | 可以将任何可调用对象封装为任务,使任务可以被放入队列 | 简化了任务的创建和结果的传递,但需要手动管理其生命周期 | 高 |
并行库与线程池结合
以下是使用 std::thread
,std::future
,std::promise
,std::async
和 std::packaged_task
的自定义线程池实现。
#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
class ThreadPool {
public:
// 构造函数: 创建指定数量的工作线程
// Constructor: creates the specified number of worker threads
ThreadPool(size_t num_threads);
// 析构函数: 关闭所有线程并释放资源
// Destructor: stops all threads and releases resources
~ThreadPool();
// 任务入队函数: 将任务添加到任务队列中
// Enqueue function: adds a task to the task queue
template <typename F, typename... Args>
auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>;
private:
// 工作线程执行函数
// Worker thread execution function
void worker();
std::vector<std::thread> workers; // 工作线程
std::queue<std::function<void()>> tasks; // 任务队列
std::mutex tasks_mutex; // 保护任务队列的互斥锁
std::condition_variable tasks_cv; // 通知工作线程的条件变量
bool stop; // 标记线程池是否停止
};
ThreadPool::ThreadPool(size_t num_threads) : stop(false) {
for (size_t i = 0; i < num_threads; ++i) {
workers.emplace_back(&ThreadPool::worker, this);
}
}
ThreadPool::~ThreadPool() {
{
std::unique_lock<std::mutex> lock(tasks_mutex);
stop = true;
}
tasks_cv.notify_all();
for (auto &worker : workers) {
worker.join();
}
}
template <typename F, typename... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> {
using return_type = typename std::result_of<F(Args...)>::type;
// 创建 packaged_task,包装任务,将任务与 future 关联
auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
std::future<return_type> result = task->get_future();
{
// 将任务包装为 std::function,并添加到任务队列
std::unique_lock<std::mutex> lock(tasks_mutex);
tasks.emplace([task](){ (*task)(); });
}
tasks_cv.notify_one(); // 通知一个工作线程
return result;
}
void ThreadPool::worker() {
while (true) {
std::function<void()> task;
// 从任务队列中获取任务
{
std::unique_lock<std::mutex> lock(tasks_mutex);
tasks_cv.wait(lock, [this]() { return !tasks.empty() || stop; });
// 如果线程池已停止且没有剩余任务,则退出
if (stop && tasks.empty()) {
return;
}
task = tasks.front();
tasks.pop();
}
// 执行任务
task();
}
}
此实现中,请注意以下关键部分:
- 构造函数初始化线程池并创建指定数量的工作线程。
-
enqueue()
函数是任务入队方法,可以将任务添加到任务队列中。它会创建一个std::packaged_task
并将任务与关联的std::future
对象关联起来。该方法返回一个std::future
对象,调用者可以使用它来获取异步任务的结果。 - 线程池内的工作线程会等待并从任务队列中获取任务。执行完任务后,任务所对应的
std::future
对象将变为就绪状态,可以获取任务结果。 - 析构函数会停止所有工作线程并释放资源。
这个线程池提供了基本的线程管理功能,你可以根据需要进行扩展以支持其他功能,例如控制线程数量或提供任务优先级。
结语
在我们的探索过程中,我们已经深入了解了C++并发编程的强大功能和广泛应用。然而,学习这些技术只是开始。真正的力量来自于你如何将它们融入到你的日常工作中,以提高效率和生产力。
心理学告诉我们,学习是一个持续且积极参与的过程。所以,我鼓励你不仅要阅读和理解这些命令,还要动手实践它们。尝试创建自己的命令,逐步掌握C++并发编程,使其成为你日常工作的一部分。
同时,请记住分享是学习过程中非常重要的一环。如果你发现本博客对你有帮助,请不吝点赞并留下评论。分享你自己在使用C++并发编程时遇到的问题或者有趣的经验,可以帮助更多人从中学习。
此外,我也欢迎你收藏本博客,并随时回来查阅。因为复习和反复实践也是巩固知识、提高技能的关键。
最后,请记住:每个人都可以通过持续学习和实践成为C++并发编程专家。我期待看到你在这个旅途中取得更大进步!
阅读我的CSDN主页,解锁更多精彩内容:泡沫的CSDN主页文章来源:https://www.toymoban.com/news/detail-448012.html
文章来源地址https://www.toymoban.com/news/detail-448012.html
到了这里,关于C++并发编程:std::future、std::async、std::packaged_task与std::promise的深度探索的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!