【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索(一)https://developer.aliyun.com/article/1464316
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
的对象。
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 的比较和选择。
【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索(三)https://developer.aliyun.com/article/1464318